Rust Programming Basics
Cyfrin Updraft Courses · 3:03:54
Learn the Rust language foundations needed before moving into Rust-based smart contract work. Tasuku Nakamura explains variables and functions, data types and collections, control flow, ownership and borrowing, error handling, modules, traits and asynchronous code through examples and exercises. This recording teaches general Rust; it does not build an Ethereum or Arbitrum Stylus application. Basic programming experience is recommended.
Speakers
Topics
From Cyfrin Updraft Courses, hosted by Cyfrin.
Transcript
Welcome to the Rust crash course. In this video, I'll explain what you'll learn in this course, topics that are not covered in this course, why you should take this course, and the prerequisites. Let's start with what you will learn in this course. In this course, you'll learn basic to intermediate Rust. One of the confusing part of Rust programming language is the ownership and the borrowing rules. This will be explained early in the course. So, you will have a lot of opportunities to apply the concept of ownership and borrowing rules when you write programs in Rust. At the end of the course, we'll touch a little bit on async await programming. Next, let's move on to the topics that are not covered in this course. The topics that are not covered in this course are closures, smart pointers, and some of the concurrency concepts such as threads, channels, and
mutexes. The reason why we did not cover these topics in this course is because this course is focused on learning enough Rust so that you can write smart contracts in Rust and audit smart contracts that are written in Rust. Once you take this course, you should have a solid understanding of Rust so that these topics are not so difficult to learn on your own. Why should you take this course? By taking this course, you'll learn enough Rust so that you'll be able to write and audit Rust based smart contracts such as Salana and Arbitum Stylus. And finally, the prerequisites. Basic programming concepts such as variables, functions, hashmaps will not be explained in this course. It is assumed that you are familiar with these concepts. Having prior experience with another
programming language such as Python, JavaScript, C, Java, and etc. will help. We'll be using the terminal to compile the Rust code and execute the test. So, you should also be familiar with working with the terminal. In this course, I'll be using a Linux terminal. And finally, the code example will be shown using a code editor. In this course, I'll be using VS Code. Okay, these are the prerequisites. Next, let's move on to course setup. In this video, I'll explain how the course is set up and how to do the exercises. The first step is to get clone this repo. The name of the repo is called Rust Crash Course. To clone this repo, head over to the siphon/ rust crash course and then click on this button called code and then copy this URL. Inside your terminal, type get
clone and then paste the URL. This will clone the repo into your computer. Okay, so that's the first step. The next step is to install the Rust programming language. This is explained in the next video. Okay. And then moving on. In this course, I will be using VS Code to show you some code examples. If you're also using VS Code, then one plugin that you should install to work with Rust is called the Rust Analyzer. If you're new to VS Code and don't know how to install plugins in VS Code, here's a quick explanation. Click on this icon called extensions. Inside the search bar, type Rust. One of the result is called the Rust Analyzer. Click on this. I already have Rust Analyzer installed. If you have not installed it yet, you'll see a button over here to install the Rust
Analyzer. This is a useful extension that gives you features such as code completion and syntax highlighting. You'll be able to take this course without installing VS Code or the Rust Analyzer, but having these tools might help you write your Rust programs. Next, I'll explain how to do the exercises. First step is to go back to the REMIE.md file, which is inside this repo. The topics that are covered in this course are listed here. For example, we have data types, control flow, ownership, and so on. For each of these topics, there's a link. All of these links will take you to a folder where there's exercises and solutions. For example, clicking on scalar types, this will take you to the folder called topics. And then under
scalar, you can see that there are two folders, exercises and solutions. The files inside both of these folders are almost the same. The only difference is is exercises is where you will write your code. And if you're getting stuck, you can look at the files inside the solutions. Here you'll find the solutions to the exercises. To do the exercises, you'll find a readme file. For example, clicking on scalar exercises readme.md. This will give you instructions on how to execute the example and how to do the exercises. For example, here we have exercise one. Fix the function eq inside source/lib.rs RS.
Inside source.lib. RS. You'll see the exercise that you'll need to do. Now, you might have noticed that there are functions called todo. These are called todo macros. They allow the code to be compiled without actually putting in any code inside. To do the exercises, you'll need to remove these to-do macros. And finally, if you're stuck or if you simply just want to see the solutions, you can head over to the folder solutions under source and you'll find your solution inside here. Now I don't want to spoil the exercise so I won't open this file to reveal the solution.
The code examples that I will be showing you in this course can be found under exercises examples and then clicking on the file. For example, clicking on scala rs. Here's the code example that I will be using when I teach about the topic for scala types. And again to execute this code, the instruction is given here under the section called example. If you execute this code, this will execute the example that is inside here. The final thing that I'll mention about setting up for this course before we get started are the resources. Navigate to the readme file and scroll all the way down.
There's a section called resources. Under here, you'll find links that are relevant to Rust. In this course, we won't be using these links, but if you're curious, go ahead and check it out. So, with that said, let's move on to installation. Installing the Rust programming language. To get started with Rust, we need to install two tools. The Rust compiler and a tool called Cargo. I'm inside the official website of the Rust programming language. To install these tools, I'll first click on a button called get started. Next, scroll down and you should see a command on how to install these two tools. In this video, I'll go through the installation of my Linux computer. If you're on Windows, you might see a different command from what is being shown over here. The first step is to copy this command. Next, inside my
terminal, I'll paste the command. As it says over here to get started, you may need to restart your current shell. So, that is what I'm going to do now. I restarted my terminal. Let's see that the Rust compiler and cargo is installed. To check that the Rust compiler is installed and to see which version of the Rust compiler is installed, I'll type rust C- version. Okay, so on my computer, the Rust compiler for version 1.87 is installed. The other tool that I want to check that is correctly installed is cargo. So I'll type cargo- version. Okay, it looks like cargo is also installed. Now let's talk about cargo. What is cargo? Cargo is a official package manager and a build system. When we build projects using
Rust, we'll be using cargo to initialize the project, build the code that are written in Rust, install other Rust dependencies, and to test our code. Here I'll show you some basic commands using cargo. Let's begin by initializing a Rust project. To do this, I'll type cargo in it followed by the name of the project. Let's say hello rust. Okay, so this will create a folder called hello rust. Let's go into this. And inside here, you'll notice that it initializes some folders called cargo.tumble. Inside this file, you'll write some basic configuration for your cargo project.
When you install dependencies into your project, this will also be listed here. Okay. And then moving on, we have the source folder which contains a file called main.rs. Inside here, there's a function called main. And inside the main function, there's a function that will print hello world. To compile this code, I'll open my terminal and then type cargo build. And this will compile all of the Rust code inside the source folder. So if you make some kind of syntax error, let's put in some invalid code and then try to compile it. You'll see that the code does not compile. So that's the first command, cargo build.
It will compile the Rust code under the source folder. The next command I will explain is called cargo fmt. What this will do is it will format the code inside source. For example, let's say that we have a messy code and we wanted to make this pretty. Then what you can do is open your terminal and then type cargo fmt. Now let's go back to our code. You'll notice that the code has been formatted. Unnecessary white spaces are removed and incorrect indentations are fixed. So that's a useful command. Cargo FMT. It will make your code pretty. Okay. Next, let's try to execute this code. To do this, inside the terminal, I'll type cargo run. When you type cargo run, it will look for a file called main.rs under the source folder
and then execute the code inside the main function. Inside the main function, we're printing hello world three times. So, inside the terminal, you can see that it printed the text hello world three times. And finally, if you have tests inside your cargo project, then inside your terminal, you can type cargo test to execute the test. In this case, there are no tests. So the number of tests that ran is zero. So these are the four basic commands that you'll be executing when you build your project using cargo. Cargo in it to initialize a project, cargo build to build a project, cargo fmt to make your code pretty, and cargo test to run the tests. In the previous video, we set up a simple Rust project using cargo. It has a single file called main rs inside the source.
Inside here, it has a single function called main which will print hello world three times. There's two things that I want to do in this video. First, I want to explain some of the components inside this file. The main function and the function called print ln with a exclamation mark. Furthermore, in the course, I'll be showing you some examples of Rust code. These code will be executable and they will be located in a special folder outside of the source. So I'll also explain how to execute those examples. So in the course you'll see some Rust examples that you'll be able to execute. These examples will be located in a folder called examples. This is a special folder that is recognized by cargo.
Maybe you have some code inside the source folder where you want to test out as examples. In this case you will put your code inside examples. Similar to the main function, the code inside the examples will also be executable. So the first step inside the examples folder I'll create a file called hello.rs and then going back to main.rs I'll copy all of this code and then paste it inside here. Also remove the comments that we had from the previous video. Okay. So this is our main function and let's focus on the main function and this function called print len. But before we do that inside the examples of this course you'll see a special heading that looks like this. # exclamation mark allow unused. This is called an
attribute and it is a metadata for the compiler. Here it tells the compiler to ignore whatever variable or functions that are not used. Without this attribute, it will show warnings inside this file for whatever functions and variables that are not used which makes the code example hard to see. Okay, so that is the attribute. Next, let's move on to the main function. What is the main function? The main function is an entry point for the Rust programs. Inside the terminal, when we type cargo run, it's going to look for a function called main inside source and it's going to look for a file called main.rs. But this example over here is located under examples and the name of the file is called hello. RS. So to execute this file, you'll need to say cargo run-
dashample followed by the name of the file without the RS extension. So this will be hello and then we can see that our file hello. RS was executed and it printed out the string hello world. So that is the main function. It is an entry point for the Rust program. In our code example, it's a simple code. Simply print ln hello world. So what is this function? Print len with a exclamation mark. First it looks like a function but actually it is not a function. Functions that end with a exclamation mark are called macros.
Macros are code that generate rust code when the code is compiled. So when this code is compiled, it transform this part of the code into actual Rust code. Here the actual Rust code that will be generated when the code is compiled is instruction to print this text into the console. So that's what this function print ln exclamation mark is doing. In summary, in this video we looked at attributes. Specifically, we looked at this attribute which suppresses warning for unused variables and unused functions. Next, I explained what the main function is. It is an entry point for the Rust programs. We also looked at macros. Macros are code that generates Rust code when the code is compiled. The print ln is also a macro that prints
text to the console. And finally to execute a binary inside the folder examples we had to type cargo run-ample followed by the name of the binary. In our example this was hello. Variables in Rust are immutable by default. To make them mutable you'll need to prefix it with a keyword mute. Let's look at example. So let's say that we have a variable called x. x is equal to 1. Now in other programming language if you wanted to update this variable x all we have to do is say x plus= 1. This will increment the variable x by one.
However, in Rust, variables are immutable by default. This means that once you declare a variable, you cannot change the value. To make this mutable, we're telling Rust that the value of this variable may change. Then you'll need to prefix this variable with a keyword called mute. Now, you may have noticed that although Rust is a statically typed language, we didn't have to declare the type of this variable. This is because in most cases, Rust will infer the type. So for example let's say let y is equal to minus1. By default integers have the type i32. So this is the same as saying let y equal to of the type i32. So this code let's rename this to z is equal to minus one.
These two code are the same thing. In the first example we're explicitly saying that the type of y is a i32. In the second example rust infers the type. The default integer type in rust is an i32. These two are the same. We can also redeclare the same variable with different values and also with different types. This is called a shadowing. Let's look at example. So let's say that x of type i32 is equal to 1. We can redeclare this variable x as something else. We can say that x is now equal to 2. And furthermore, the type that we declare does not have to be the same. So we can say that x of type boolean is equal to true. So this is shadowing which means
that we can redeclare the variable having different values and different types. In some rare cases, you need to tell the Rust compiler what the type of the value is. But maybe you don't know the type or maybe you're feeling lazy and you want Rust to figure out the type. In this case, you can use the underscore placeholder to tell Rust to figure out the type. So for example, let's say let X of type placeholder. This tells Russ to figure out the type. And let's put a value. Let's say 1 2 3. So this is a simple example, but when you have complex examples, this can become useful. It tells Russ that whatever type that it has to be, please figure it out. This is an example of a type placeholder. If you have a variable where you know that the value is not
going to change, then we can declare the variable as a constant. So far we've seen that variables are declared with the let keyword. However, for constants, you declare with the keyword const followed by the name. Let's say num. for the type let's say U32 unsigned integer of 32 bits and say the value is equal to one. Now since the constants are immutable and the value of the constant is already known when we write this code this constant declaration doesn't have to be inside the main function. It can be outside of the main function. So these variables will live inside the memory whereas the constant will live inside the compiled code. The value for num will be hardcoded inside the code that is compiled. Unlike the variable over here where memory will be allocated
when the main function is executed. Okay, moving on. Let's look at some examples of using print to print these variables. Let's say that we have a variable called x equal to 1. How do we use the print ln macro to print this variable? The simplest way is to say print ln double quotes curly braces. This is where the variable x will go in. And then outside of the double code, we put in this variable x. So let's say x is. And then let's execute this code. cargo run- damp example. The name of the file is called bar. Okay. And then it says x is one. For simple variables, we can also put the x inside these curly braces. So what we could have done is
put our x inside here and then remove the rest of the code. Let's execute the code again. And then we get x is one printed out twice. Inside these curly braces, we can also specify the variable that has to go in using positional arguments. Let's copy this and then paste it here. Let's print out the sum of adding x to itself plus equals something. So what we can do is put in x here. Put in x here. Inside here, we won't be able to put in x plus x since the variable that we put in here must be a simple evaluation of the variable. So to do this we might have to say that Z is equal to X + X and then put a Z inside here. So this is one way of
printing the sum of X to itself. But there's another way using positional arguments. Here we'll put in a zero and then here we'll put in a zero and then here we'll put in a one. This is a positional argument relative to the inputs that we're going to be passing here. For the zero argument we'll put in X. So this X will be printed out to here and here. And then for the positional argument of one, you'll put in x + x. So this line is saying for this curly braces, put in the zero input. And for this value over here, put in the zero input. And for this curly braces over here, put in the first input. The zero input will be x. And the first input will be x + x. Let's execute the code.
And we get 1 + 1 is equal to 2. The last useful option for printing is the debugging feature. So let's say that we wanted to print X. X is a simple data type, but if the data type was complex, then you might want to use the debug feature. This will print out the variable with details in a way that is easy to read. To do this, you put in colon and then question mark. We want to debug the variable X. So this is how you will do it. Another way to do it, if when you debug this variable X and it turns out to be multiple lines, then you can make it look prettier by putting in a hashtag over here. So these are two ways to debug a variable. They become useful when the data type that you want to debug is complex. Let's execute this
code. Execute the code again and we get debug x1 and debug x1. So in summary, in this video, we looked at some ways to print variables using the print lm macro. We also looked at different ways that the variables can be declared, but the most important thing that you should remember from this video is that variables are immutable by default. And if you want to modify them, then you'll need to prefix the variable with the mute keyword. Let's look at some examples of how to write functions in Rust. To declare a function, we start with the keyword fn followed by the name of the function. Let's call this add. As the name suggest, it will take in two inputs and then return the sum of the inputs. The first input I'll name this X of the type U32 unsigned integer of 32 bits and then Y will be of the same type
U32. It will return a sum. So it will return another type of U32 and inside here let's say return X + Y. This function takes in two inputs X and Y and then returns the output of the type U32. Now if you're curious on how you return this function so that it returns multiple outputs. For example, let's say we wanted to return a U32 and a boolean. And as you can see from here, all you have to do is wrap our outputs in parenthesis. And over here we'll say x + y and then return a boolean. Let's say true. So this is an example of returning multiple outputs. Now in rust the return keyword is optional. So to show you this I'm going to first revert some of the
changes that I made. I'm going to remove the second output that is returned. So now our basic example is back to returning a single output of the type U32. The keyword return is optional in Rust. So next I'm going to rewrite this function without the return keyword. To do this I'll first name this function as add with return. Now let's look at example of how we will turn this function into a function that returns the same output without using the return keyword. I'll rename this function to add. Okay, first I'll remove this return keyword. The last statement inside the function is what is being returned. And when we return the value, we also need to remove the semicolon. For example, if
we had some code over here like printing X and Y. This function will print the value of X and then print the value of Y and then it will return the sum of X + Y. Let's compare this syntax with the example above. Here we're saying return and then followed by the semicolon. If you wanted to do the same thing without the return keyword, we will first omit the return keyword and then we will also omit the semicolon. So these two code do the same thing if I remove this print element. Now let's call this function inside the main function. I'll first create two variables X and Y. Let's say that X is equal to 1 and Y is equal to 2. And then I'll call the function at passing in X and Y. I'll assign the
output to a variable called Z. And then let's print this out. X + Y is equal to Z. Let's execute this code. cargo run- dashample. The name of the example is called funk and then prints out 1 + 2 is equal to 3. Now you can also have a function that doesn't return anything. So let's look at example of a function that does not return anything. fn print. I'll create a function that will take in a string as input and then print the string multiple times. So for the input, it's going to take in a string and it's not going to have any output. So we don't need to do this arrow followed by the type syntax. Inside here I want to print this string s five times. Print ln. So let's say s 1 2 3 4 and five.
Let's call this function inside the main function. Print. And then here we'll need to pass in a string. For this example I'll pass in a frog emoji. And then we need to convert this into a string by typing to string. The reason why this is not a string and we need to convert this into a string. I'll explain this in another video. For now, all you need to know is that we converted a emoji into a string and then we call the function print which will print this frog emoji five times. Let's actually execute this code. Run the code. Then we can see five frogs printed out into the console. Let's look at data types in Rust. We'll begin by looking at scalar types. Scalar types are data types that represent a single value. These are
signed integers, unsigned integers, boolean, and characters. Sign integers represent integers that can be both negative and positive. The range of the sign integer depends on the type of the sign integer that you're using. A signed integer of type I8 will range from -2 raised to the^ 7 up to 2 rais^ 7 - 1. A signed integer of type i16 will range from minus2 raised to ^ 15 up to 2 rais^ 15 minus 1. There's also i32, i64. And finally we have i128. These numbers over here 86 32 64 and 128 represent the bits that is needed to represent these ranges
of numbers. The range will be in the form -2 raised to the^ of n minus1 up to 2 raised the^ n -1 -1. Let's move on to unsigned integers. These will represent numbers that are not negative. Non- negative numbers. Similar to signed integers, there are several types for unsigned integers. The range will come in the form from 0 to 2 to the n minus one. Where this n will represent the number of bits that the number stores. For example, a number of type u8 will support a range from 0 up to 2 8 minus one. Type U16 will represent number ranging from 0 to 2 to the 16 minus one.
We also have U32, U64 and finally we have U1 to8. So these are the unsigned integers that are supported in Rust. Let's move on. There's also integer types that depend on your computer architecture. These types are called I size for signed integers and U size for unsigned integers. If your computer architecture is 32 bits, then this I size will be I32 and this U size will be U32. If your computer architecture is 64 bits, then I size and U size will both be 64 bits. The data type U size is particularly important. This is the data type that is returned when you get the length of vectors and arrays. I forgot to mention that floatingoint numbers are also scalar type. These are F32 and F64.
Floating point numbers represent decimal numbers. Okay. Okay. And then moving on, we have the boolean. To declare a boolean type, you would type bool. So that's boolean. Let's move on to characters. A character is a single uni code value. For example, you can store any letters from a to z. Notice that for characters, we use a single quote. If you use a double quote, then this will turn into a string. You can also represent a single emoji as a character. For example, here we store a crab emoji into the variable called e. Next, let's talk about type conversion and how to get min and max value for these scalar types. Let's say that I have a variable name I of type i32. And let's set this equal to minus1. Let's convert this
signed integer into a unsigned integer. That's U. Let's use the type U32. To convert the variable I of the type I32 into a U32, you say I as U32. Let's print this out. It'll say I as U32 is equal to U. Execute the code and we get minus1 as U32 turns out to be a big number. This is because minus1 as a 32-bit representation puts a one at the leftmost position. So when you convert it into a U32, this becomes a large number. So anyways, this is an example of how to do type conversion. Now if you wanted to get the maximum of U32 or I32, here's how you would do it. For example,
let's say that I max is equal to I32 colon max all caps. And then if you wanted to get the let's say U min then you can say U32 min. Let's print these out as well. Imax is equal to Imax and U min is equal to U min. Execute the code and get IAX is equal to this number and minimum for U32 is equal to zero. Okay, so these are some examples of scalar types that are available in Rust.
Overflows and underflows don't panic when compiled with the release flag. When you release your code for production, you will compile the contract with the release flag turned on. And when you do this, overflows don't panic. So let's look at some examples. So let's say we have a number let mute X. Let's set this equal to the maximum of U32. U32 max. This is the maximum number that can be represented for U32. And now let's see what happens when we add one to this value. X plus equals 1. Okay. And then let's print these out. We'll print the maximum of U32 along with the new value for X.
Print len U32 max and the value for X. Execute the code and the main function panics with the error attempted to add with overflow. This is because we didn't run this code with the release flag enabled. So now let's run the same code using the release flag. And this time notice that the code executed without any errors. Here is the maximum of U32. And then after adding one, X is equal to zero. If this number did not overflow, you will see that X be larger than this number. However, because X overflowed, it wrapped around and became zero. How would we deal with overflows and underflows? There are two functions available in the Rust programming language to handle overflows and
underflows. The functions are called checked add and wrapping ad. In this example, I'll be using the functions that are available for the type U32. But the same functions are also available for all of the types. So this will be for U64, U18 and so on. So now let's look at example of check add. The function checked add will return none on overflow. We haven't discussed what none is yet. So here I will give you a quick explanation. None represents either two values. When something is successful, the actual data will be stored in another data type called sum. If there was some kind of error, then it would return a none. In this case, if there was an overflow, this will be an error.
So it would return a none. If there was no overflow, this will represent the okay case. So it would return sum with the actual number inside. And then we have the wrapping ad which explicitly allows overflow. So the behavior will be the same as what we saw over here when we ran the code in release mode. Let's start by looking at check that let x is equal to u32 checked add. Let's add the maximum value of u32 with one u32 max + one. Then let's print the result out print ln. Here I cannot simply print this value x out. So we'll tell Russ to print this out in debug mode. That is colon and then question mark. Okay, let's execute the code and we get check
that is equal to none. This is because when we added one to the maximum of U32, then the number wrapped around so it returned a none. However, if we change this to some regular number, let's say three and then execute the code again, we get checked is equal to sum with four inside. So that's an example of checked add. Next, let's move on to wrapping ad which explicitly allows overflow. We'll use this example again. Then paste it here. Say change the function name wrapping ad. Execute the code again and get wrapping ad is equal to four. If you try to introduce overflow again and then execute the code, this time the function wrapping ad return zero. So these are
two functions that allow you to handle overflow and underflow in rust. Check that and wrapping ad. TPLE is a data type that represent a collection of values. This collection has a fixed size known net at compile time and the collection can have various types. Let's create a tpple. Let's say that t for the first element let's store a boolean. For the second element let's store a character and for the last element let's store u32. This is equal to let's say true and then next is a character let's put in a a and for the number let's put a one. Okay. So this is how you would create and declare a tpple. How do we access the elements inside a tpple? Well we will do t.0 zero to access the zero element, t do one to access the first
element and t do.2 to access the second element. Let's print these out. Execute the code and we get true a and one. You can also create a empty tpple. This is also called a unit type. To create an empty tuple, let's say that t is equal to simply parenthesis without putting any values inside. Now this is called a unit type. This unit type is actually what is being returned when you call function that does not return anything. For example, here I have a function called no return. This function does nothing and also returns nothing. This function is the same as the function return empty tpple. This function does nothing and then returns a empty tpple.
This empty tpple that is being returned is called a unit type. Now you might be thinking that this unit type is useless. However, once you start working with Rust, you'll see this unit type used often. It's usually used in combination with a enum called result. Result is a enum that you will use when something is successful. On the other hand, when there's an error, you would want to return something else. However, what if you wanted to express that something happened without any errors? In this case, you would use the empty tpple. You would return okay saying that there was no error, but there's no value to return. So, I'll just give you an empty tpple. On the other hand, if there was an error, for example, you might want to return the error enum with some error message inside. So that's the empty
tpple. Next, let's move on to nested tpples. How do we create a nested tpple? Let's name this as nested. Let nested. Inside a tpple, we'll create more tpples. For example, the zero element will be a tpple holding a character and maybe a float 1.23. And then the next element inside this tpple will be another tpple. For example, let's put in true a U32 1 U32 and maybe a I32 minus one I32. And for the last tpple, let's use a empty tpple. Now notice that inside this tpple, we can have tpples that have different data types and also have different size. A tpple of size two
and then here we have a tpple of size three and here we have a tpple of size zero. Okay, so how do we access a nested tpple? If you wanted to access the zero element, then we will say nested. This is the same as what we saw over here. Okay. From the zeroth value of this nested tpple, it returns another tpple. So let's say how do we access the first element inside this tpple. Then we will say one. Now to be clear, we also need to wrap this first part with a parenthesis. So we will say nested.0 parenthesis.1.
This will access the zero tpple. And then from the zero of tpple we will access the first element. So this will return 1.23. Let's print this out. Execute the code and we get nested is equal to 1.23. We can also dstructure a tpple in rust. Dstructure is a shorthand way to access the elements inside the tpple and then assign it to a variable. For example, let's say that we wanted to assign the variable name abc to the values inside this tpple t. And the way we would do it is say let a b c is equal to t. This will match the values of the tpple boolean character and u32 into the variables a b and z. Here the variable t
is reassigned to the empty tpple. So I'll redeclare this over here. Let's print the variables a, b and z. Execute the code and we get a is equal to true, b is equal to the character a and c is equal to 1. Here we dstructured all of the values inside the tpple t. But what if you wanted the element at index one and not the element at index zero and two? In this case, we can use the underscore to tell Russ that we don't care about the zero and the second element in the tpple. So instead of declaring this as a variable a, we put an underscore here and the underscore here as well. This would tell Russ to ignore the first and the last value of the tpple. And for the final example, let's look at how to return multiple values from a function using a tpple.
Let's say that we have a function called return many. And what we want to specify is that we want to return a U32 and a boolean. So you want to return a U32 and a boolean. To return multiple values, we need to wrap this into a tpple. And then inside here, let's return a tpple of U32. Let's say 1 U32. And boolean, let's return a true. Okay. So this is how you would declare a function that returns multiple outputs using a tpple. Let's call this function inside the main function. So a return menu. And finally we can use the dstructuring syntax to assign the outputs of this function to variables. So let's say let the first output we'll assign it to a variable
called a and the second output we'll assign it to a variable called b. Okay. So this is an example of returning multiple outputs from a function call and then using dstructuring syntax to assign it to variables. Arrays are collection of elements with length known at compile time. Slices are a collection of elements with length not known at compile time. The reason why that the length of a slice is not known at compile time is because you can have a code where you take an array and then slice it up inside your program. The length of this resulting slice is not known at compile time. It is only known when the code executes. Therefore, the length of the slice is not known at compile time. Let's look at some examples of array and slices. Let's create an array. I'll name this R and
that R. The type of this array will be U32 with the size three. Since the length of the array must be known at compile time here, we need to specify the size as a number that is known at compile time. Inside here, let's put in the numbers 1 2 and 3. Okay. So, this is an example of a array. To access an array, you can access it by the index. For example, you can say r of zero. Let's execute this code and we get the values r of0 is equal to 1. To make this array mutable, we'll need to prefix this with the mute keyword. For example, let's update the element at index one.
R1. Let's set this equal to 99. Now, let's say that we wanted to create an array with 10 elements and all of the elements are initialized to zero. One way you can do it is to manually put in zeros. But there is an easier way to do it. Here's how. Let's call this R again. This will be an array of type U32 having the size 10. And this is equal to 0 followed by 10. This tells Russ to create an array where all of the elements are set to zero. To show you this, let's print this out. Execute the code. We get an array where all of the elements are set to zero. Next, let's move on to slices. Slices represent a slice of a vector or array. For the examples below, I'll be using this nums
array. And we'll slice this nums array in different ways. For example, if you wanted to get the first three elements from this nums array, let's name this variable as the type will be a reference to an array of type i32. And this is equal to we'll slice the nums array from zero to three. We want the first three elements, but not including the element at the third index. Here the zero is optional. How about if you wanted to get the last three elements? The last three elements are indexes 7, 8, and 9. So we'll start our slice from index 7. And then we also want to include index 9. So 2 9 we add a one. So this will be 9 + 1 is 10. Here the last index is optional.
So we can remove it. This will get the last three elements from the nums array. Okay. How about the middle four? How do we get the middle four? The middle four are indexes 3, 4, 5, and six. So the slice will start from index 3. It will end at index 6. So this will be six + one which is equal to seven. So this is how you will get the middle four. Let's print this out. Let's say print len mid4. Execute the code and the middle four elements are 2 minus 3 3 and minus4. Okay. So these are some examples of working with arrays and slices and rust.
The two most common data types that represent a string is string with a capital S and a string slice. So when would you use one over the other? You will use string with a capital S when you need ownership or when you need to modify the string. When you need a string for read only purpose or when you hardcode a string then you will use a string slice. What does all this mean? Let's look at some examples. There are several ways to create a string. Let's say that message of the type string with a capital S is equal to. The first way you can create a string is by calling string colon from and then inside here you put your string literal for example hello rust. Another way is to start with the string literal hello rust and then
say dot to string. You can also get the length of the string by calling message.length. This will return the length of the string. Here the type of the length that is returned is use size. Remember use size is unsigned integer and the number of bits that represent this unsigned integer depends on your computer architecture. Okay, this is string. Next, let's take a look at string slice. There are several ways to create a string slice. You can start out with a string and then cast it into a string slice or if you create a string literal, then it is automatically a string slice.
So, let's say that we started out with a string. To create a string slice, let's say s will be a string slice is equal to. We can simply create a string slice by referencing this string message. Another way to create a string slice is to create a slice of this string. For example, let's say that we wanted to get the first five letters from this string called message. We'll start from zero and then go all the way up to the fifth character. So when we print this out, we're going to get the word hello. Let's print this out. print len and say s is equal to and then print s execute the code and we get s is equal to hello string literal are also string slices so
let's say that s we'll hardcode this as a string is equal to hello world so in other programming languages this will turn out to be a string however in rust this is a string size here we make a distinction between string with a capital s and a string string slice and pretend sign str. This hard-coded string that you see over here is a string slice. To convert this into a string, you'll need to say s dot to string. This will convert the string slice, the string literal into a string. So let's say let x is equal to. And to make sure that the type checks out, let's specify this as a string. So now we've seen some
examples of converting a string into a string slice and then starting from a string slice and then converting it into a string. Now in most cases, Rust will automatically convert a reference to a string into a string slice. So when will this become useful? Let's look at example. Consider that we have a function that will take in a reference to a string as input. In this example, this function name is called print and it will simply print the string out. Inside the main function, we can call this by first creating a string. Let's take this example message and then calling print passing in a reference to this message. Now if you had a string slice and then we wanted to call this function this code will not compile since the function print expects a
reference to a string. However, here we're passing a string slice. We can check that the code does not compile by trying to run the code. Try to run the code and we get an error back. Expected a reference to a string found the string slice. So we can improve this code by actually changing this string to a string slice. Execute the code again. And now the code executes without any errors. So what's going on here? In the first call to the function print, we're passing in a reference to a string. Well, it looks like we're passing in a reference to a string. However, Rust will automatically convert a reference to a string into a string slice. So that is what you're seeing over here. Even though we're passing a reference to a string, Rust converts it into a string
slice so that we can call this function. And now we can use this function print to both pass in a reference to a string and a string slice. So this also highlights the example that when you're working with read only strings, it's easier to work with a string slice than a string. In this example, we're simply printing out this string. So if he had this type as a reference to a string, then we can only print a reference to a string. However, by changing this input to a string slice, we can both pass in a string slice and a reference to a string. Let's look at two more examples.
How to append a string slice to a string and string interpolation. To append a string slice to a string, we'll start with the string message. And since we're modifying this, we'll need to prefix it with the keyword mute. Now, to append a string slice, all you have to do is use the plus operator. So say message plus equals let's add world to the message we're appending the string stice world. So when we print this out and then execute the code we get hello rust world. So that's an example of appending a string stice to a string.
The last example let's look at string interpolation. Sometimes you want to build a strings out of other strings. Some of these strings will be hardcoded and other ones will be a value stored in a variable. We can probably use this plus operator to build this string that we want. However, there's an easier way by using string interpolation which is done by the format macro. So let's look at example. Let's say that we have two string literals rust and then the emoji of a crab. We want our resulting string to be hello rust followed by the crab emoji. One way we can build this is to say let mute s is equal to hello dot to string. And to this we first add a space. Next we add the variable lang
which stores the value rust. And then we add another space. And finally we add the emoji. So this is one way of constructing this string given that we have these two variables. But there's an easier way using the format macro. To use it, all we have to do is call the function format. And then inside the double quotes, we first put in the word hello, followed by the first string that we want to put in, followed by the next string that we want to put in. And finally, outside of the double quote, we'll put our variables lang, and then emoji. This is the same syntax for print ln. So this will return a string. So let's assign this to a string called s and then print it out. Print len.
execute the code and get hello rust with a crab emoji. So that is an example of string interpolation. In this video, we looked at two common data types that represent string. String with a capital S and a string slice. String is used when you need ownership or when you need to modify the string. Now we haven't discussed what ownership is yet. Later in the course, I will explain what ownership is. On the other hand, if you only need to read the value of a string or need to hard code a string, then a string slice is a better choice. Enums are data types that can express the possible values that the data type can take on. For example, let's consider a video player like YouTube. What are some of the commands that we can express as a
enum? Let's call this command. A video player can play, can have a command to stop the video. It can maybe also fast forward. So we'll have a command for skip. When we skip, we also want to put in the time stamp that we want to skip to. So we can represent this as a command called skip and then also store the time stamp. For example, U32. And likewise, we can have a command to go back. Again, the back will store a time stamp. Time stamp to go back to. And finally, we might have a command to resize the window. Let's say resize.
When we resize a window, we also need to specify the width and the height of the new video player. So let's say width, we'll represent this as a U32 and height, we also represent this as a U32. From this example, you can see that there are several ways to represent a enum. How do we use this enum inside the main function? The simplest way let's say that cmd of the type command is equal to command and let's create the simplest enome that we can from this example. The simplest one probably is plate. So this is one way to create a enum. Okay. How about skip? How do we create this enum? Well, we do the same thing and then here we say skip. Now
skip takes on a single value. So you'll need to store a single value here. For example, if you wanted to skip to time stamp equal to 10, then inside here we'll put in a 10. So that covers the case of skip and back. Back will look similar to the example for skip. How about resize? Let's say resize. And then inside here, let's say d is 100, height is 50. Okay, so these are some examples of creating an enum. Let's print this out. You'll say print ln curly braces and then cmd.
Execute the code and you'll notice that there's an error here. Command cannot be formatted with the default formatter. To fix this, we'll need to do one more thing to our enum. We'll need to put some special macros so that we'll be able to print this enum. This macro is called derive with debug. So by putting this code into our enum also works with strrus we'll be able to print this enum out. And finally we'll need to put in colon and then question mark. Execute the code again. And now we can see that the command is printed. Resize with width 100 and height 50. Next I'll show you example of how to compare the values of a enum. So let's say that we have two
values of the enum command cmd0 and cmd1. Let's try to compare these two enums to see whether they are equal or not. Print that in. And then inside here we want to return a boolean saying is command zero equal to command one. Command z is play and command one is skip. So we expect this comparison to return false. Let's give it a try. Execute the code. And now we get a compilation error saying that the enum command must implement partial equal. Back inside our code, what we need to do here is put in partial equal. Again, this is a macro. So, this will autogenerate code so that we'll be able to compare different values of a enum.
That's what partial equal will allow us to do. It will allow us to compare different values of the same enum. Now, let's execute the code again. And this time, it returns false. And obviously since the first command is play and the second command is skip and if you change this to command zero and command zero both commands are equal to play. So if you execute the code it returns true. So far we looked at how to create enums in different ways how to print it out and then how to compare different values of the same enum. Next I want to talk about the option enum and the result enum.
Option enum is often used to express a presence of a value or the absence of the value. For example, let's say let x of the type option of i32 is equal to sum of 1. This will mean that there is a value and the value is equal to 1. And if there was no value, then we can do the same thing. Copy the code from above and then set it equal to null. The second code will mean that there is no value. This option becomes handy when you're trying to access array elements.
Instead of directly accessing array elements, we can call a function called get. And if the index is inside the bounds, it will return sum with a value inside. However, if the index that we queried is out of bounds, then it will return a none. In this case, when we run the code, the code does not panic when we try to access our index out of bounds. Instead, it will return the option with the value inside or none. Okay, so let's move on to the final example. Result. The result type represents something successfully executing and then returning a value of type T or there was a error and it returns a error of type E. Imagine that you parse a string into a number. If you are able to successfully parse the string into a number, then we'll
represent this as a okay with the actual number inside. For example, let's say X of the type result. In the successful case, we'll return this type D which will be a number. For example, I32. In the error case, we'll return error as a string. If we are able to parse a string into a number, for example, let's say that we have a string 100 and then we parse it into a number 100. In this case, we'll return okay with 100 inside. However, let's say that we have a string. Maybe we have some numbers and letters and some characters that are not numbers. then this should be a error. In this case, we can represent this value the result of parsing this string trying
to parse the string into a number as a air. Let's say error fail to parse string into number. Okay, so these are two example of the result enum. It represents either something successfully happening and then returning the value or there was some kind of error and it returns an error with some kind of message. Strs are useful data types that you can use to group different data types into a single data type. For example, let's say that we wanted to represent the x coordinate and ycoordinate of a point. Let's call this a point strct point. I have two fields x of type let's say i32 and y of type i32.
Another way to create a strct is to use the syntax that resembles a tpple. So for example, let's say strruct we want to represent a point in 3D space. Let's say 3D. Now, instead of using the curly braces, we can use a tpple and then specify the data type that we want to store. For 3D, we need to store three coordinates. The x coordinate, we'll represent it as i32. The y-coordinate and the zcoordinate all as i32. We can also create a empty strruct. For example, strct empty. And finally, we can also create a nested strct. For example, if you wanted to represent a circle, a circle can be represented as a radius. Radius will be a non- negative number. So let's say U32. And then the
center of the circle. Let's say center will be a strct point. Inside the circle strct, we're storing another strct called point. So these are some examples of a strct. Next, let's look at how to initialize these and modify these inside the main function. Let's create the point strct that P is equal to point and then let's say X is one and Y is also one. Let's try to print this out. Print L and then P. Execute the code and the code does not compile saying point cannot be formatted with the default formatter. We've already seen this error before. To fix this, we need to specify this strct with derive debug. derive debug. And then over here to print this
out, we'll need to do colon and then question mark. Execute the code again. And now we get point x= 1 and y equ= to 1. So to print the circle, we'll also need to put this derive debug macro. How do we access the field x and y of this point? Well, we can access it by p.x and p doy. Let's print these out as well. Print ln px and p doy. execute the code and we get x1 and y is equal to one. How about the case for this strct over here?
Over here to access the field of this strruct point we had to specify the name of the field x and y. But for this example there is no field name. So how do we access this? We're going to access it the same way that we access a tpple. So let's say that p is equal to 3d. Let's say minus one zero and minus one. To access the zero value which is minus one, we'll do p.0 the same way that you will access a tuple. And if you wanted to access the next value, we'll do p.1.
And the last value will be stored in p.2. Let's print this out as well. Print len 3D. Execute the code and we get 3d is minus 1, 0, and minus one. Next, let's take a look at how to initialize the empty str. All we have to do is say that empty is equal to empty. Since the empty strruct doesn't store any values, this is how you initialize empty strruct. How about a nested strct? Let's say that circle is equal to circle having the radius let's say one and the center. For the center, we can either put in this point or we can enly 0. Let's try printing this circle. This
time we'll use the print ln with the debug feature. Now if you print this out, you'll notice that the circle strct is printed on a single line. If you wanted to make this pretty, then you can put a hashtag here between the colon and the question mark. Execute the code again and this time we get a prettier formatted circle printed out. Next, let's look at some operations that you can do on a strct. Let's start with a shortcut. Let's say that we have x equal to 1 and then y the variable y is equal to 1. The point field is of the type i32. So let's also make this of the type i32. Let's say that we wanted to create a strct where we store this value x for the field x and y for the field y. Let p
is equal to point where x is the variable x and y is the variable y. When the variable name and the field name are the same, we can do a shortcut. What we can do is say x and then y. So that's an example of a shortcut. Moving on, let's move on to the example of copying fields. So let's start with a point strct again. Let's say that we have a point with x= to 1 and y = 2. And then we wanted to create a copy of this point P 0, but we only want to update the field x. So the value for y will be p 0.y. So this is one way of copying the field. Now imagine the case that we have more fields in this point. For example,
Z then maybe B and then W. Now to copy the values of P 0 into P1, we will have to say Z is P 0.Z, V is P 0.V, and W is P 0.W. So that's a lot of typing to do. So here is a shortcut that you can do. Instead of manually typing all of the fields that we're going to copy, we can simply specify the part that we are going to update and then say copy everything else. And to show you this, let's print this out. Execute the code and we get P1 copy is point with X= 2 and Y= 2. In this example, the field that we copied was only the Y field. But
imagine the case that we have multiple fields that we want to copy. In that case, this syntax will be useful. We only specify the part that we want to update and then the rest of the code we're going to copy over. And finally, let's look at example of how to update a field in a strct. To begin with, let's copy this and then paste it here. We'll initialize a point. To modify this point, we'll need to specify as mute. We already know how to access the field. For example, to access the field x, we do p.x. Now, let's increment the value for p.x. We can do this by doing plus equals for example one. Increment it by one. If you wanted to update the field Y, we can also increment it. But for this example, let's set it to for
example 99. Let's print this out. Update point. Execute the code and we get update point. Point is X= 2 and Y is equal to 99. So these are some examples of operations that you can do on a strct. Vectors are like arrays. a collection of elements having the same type. The difference between array and a vector is that array has a fixed size known at compile time. On the other hand, the size of a vector can grow and shrink at runtime. To create a vector, let's say that v of the type vector, let's create a vector of type i32 is equal to back mute. This will create a empty vector.
So if you wanted to put some elements inside it, we'll need to prefix it with the keyword mute. To append elements at the end of the vector, we call the function push. Let's push in some numbers. 1 2 and then three. So what does this vector look like? Let's print it out. Execute the code and we get vector is 1 2 and three. Now if you already knew what elements to put in, we could have done a shortcut. We could have initialized the vector having the values 1 2 and three instead of initializing a empty vector and then pushing elements 1 2 and three. Here's how you do it. Let's say that B is equal to we use the beck macro beck and then brackets and then followed by the elements that we want to push 1 2 and
three. Now by default numbers default to i32. So this will have a type back of i32. But what if we wanted to hardcode these numbers but have a different type for example I8. Well here's one way to do it. We specify a vector of i8. Or there's another way we can do it. The same thing we can omit this and then say that the first number is of the type i8 and if you wanted to create a vector of u8 then you can do 1 u8. How about the case if you wanted to initialize a vector with 10 elements or maybe 100 elements all initialized to zero. Yes, you can hardcode the numbers maybe 100 times. But there's an easier way. Let's
say let b of type be of i8 is equal to be macro. We'll initialize the elements with a zero of the type i8. And we want this vector to have a length 100. So we initialize a vector having the length 100. All of the elements are initialized to zero. Let's print this out. Execute the code and we get a vector with a bunch of zeros. How do we access the elements inside a vector? The simplest way is to say brackets and then the index. For example, B1. Let's print this out. And then we get B1 is zero. Clever.
What if we try to put in an index that is out of bound? The length of this vector is 100. So what if we put in 1,000? We're trying to access a element that doesn't exist. Execute the code and you see that the main function panicked. This is because we try to access a vector element that is outside of the bound. So there's a better way to handle this case. Instead of letting the program crash, we can call the function B.get to get passing in an index here. The get function is a option of the type a reference to the value that it stores.
In this case, the value that is stored is i8. So it will be a reference to the value i8. If the index is valid, then this will return sum with the value inside a reference to the value inside. And if the index is invalid, this will return a none. So let's see this in action. Say print ln be a one. execute the code and we get B of one is sum with zero. How about if we try to access the 1,00 index? We know that this vector has length 100. So trying to access the 10,000th element resulted in a panic when we executed this code over here.
How about when we call the function get? When we call the function get, it returns a none. So this is a useful case of an option. Instead of directly accessing the vector elements and if the index is out of bound, the program crashed. However, by calling the function get, it gracefully handled the error. If there was a value, it returned a value wrapped in a enum called sum. If the index was invalid, then it returned a none. We can also mutate the vector. So, let's initialize a vector. Again, we'll need to prefix this vector with the keyword mute. Let's update the zero value B of zero. Let's set it to 99. So, this is one way of dating a vector element. Simply accessing it and then updating the value. If you wanted to remove the last element from a vector,
then we can call the method pop. The pop method will return the option with the value inside. In this case, we have a vector of i8. So the element that will be returned is an option of i8. So let's say that x of type option i8 is equal to b.op. And then let's print this out. Execute the code and we get pop with sum three. This is because the last element in this vector is a three and then we removed it. So it return a sum with three. If you card three more times then it will return sum with two, sum with one and then none. To see this let's call this three more times. So this will return a three. This will return a two.
This should return a one. And then if we call pop on an empty vector then this should return a none. Execute the code and we get pop sum three sum two sum one and then none. Similar to array we can also create a slice from a vector. So let's say that V is equal to beck having the values 1 2 3 4 and 5. Let's create a slice at S is equal to 10% sign V. What portion of this vector do we want to get? How do we want to slice this vector? Let's try to get the values 2 3 and four. The index to start is 1. The index to end will be 1 2 and 3. This will be 3 + 1 which is 4. Let's print this out. print ln slice and then put in
s. Execute the code and we get the slice is equal to 2 3 and four. So these are some examples of working with vectors in rust. Other than the vector, the hashmap is another common data type that is used often in Rust. To use the hashmap, we need to import it from the standard library. Use std collections hashmap. To initialize a hashmap, you'll call the function hashmap col. Let's create a hashmap where the key is a string and the value is a U32. This will represent scores of teams. So let's call this let scores of the type hashmap from string being the keys. U32 being the values. We want to be able to add scores to this
hashmap. So let's make this mute. To insert values into this hashmap, you'll call the function insert scores. And then specify the key. Let's say red team. string. Since red is simply a string literal, we need to convert it into a string. And then let's say that the red team has 100 points. The blue team, let's say, has 200 points. Let's print this out. Execute the code and get a hashmap with red team 100 points and then blue team 200 points. Now, given a hashmap, how do we get the value that is stored for the key? To do this, we'll call the function get on the hashmap. So we'll say scores dot get then inside here we need to pass
in a reference to a string. Here instead of passing a string we can also pass a string literal s key. For example to get the score of the red team we can pass in a string literal red. This will return let's say score is a option of the type a reference to the actual value u32. Let's print this out. Print len. Since the red team exists, we expect this to be a sum with 100 inside red score. Execute the code and we get red score is sum with 100. How about if we try to access the green team? Since the green team doesn't exist, we have not inserted any value for the green team. When we execute the code, we expect that green team score is none. If the value was inserted, it will return some with the
value. If the value was not yet inserted, it will return a none. There are several ways to update a value for a hashmap. It's a little bit tricky, so I think it is worth explaining it here. First need to get the entry from the hashmap. We get the entry by calling the function entry and then passing in the key. Here the key that we pass in must be a string. For example, let's try to get the key blue. This will be blue dot to string. So this will return the entry. But what if the entry does not exist? For example, let's say black. In this case, we'll need to call the function or insert. If the entry doesn't exist, then insert some default value.
For example, let's put in a zero. Now, this will return a mutable reference. So, let's say that score of the type mutable reference to the entry U32. Now that we have a mutable reference, we can dreference it by using the asterisk and then same score. Let's increment the score of the black team by 100 plus 100. Once we updated the score, let's get the score of the black team and then print it out. So I'll change this red to black. Execute the code and we get that the black team has score of 100. Now since the blue team has started out with 200 points, we can also use this same exact code to update the score of the blue
team. All you have to do is change this back to blue. Execute the code again. And this time blue team started out with 200. we incremented by 100. So now they have 300 points. Okay, so these are some examples of working with hashmaps in Rust. Let's look at if else statements in Rust. Let's say that we have a variable called X having the value 10. Let's execute conditional code depending on the value of this X. So let's say if X is greater than zero, then we'll print ln X greater than zero. Else if x is less than zero then we'll print x is less than zero. Otherwise if x is neither greater than zero and less than zero then we know that x is exactly
equal to zero. Print len x is equal to zero. So this is how you would write a if else statement in rust. Now there's a nice feature in rust where you can return the value from a if else statement. So let's say that we wanted to return a one if x is a positive number minus one if x is a negative number and zero if x is exactly equal to zero. The way we would do this is to say let z let's say is of the type i32 and don't forget to put a colon here. So this will return a conditional value for z depending on the value of x. Now, it's important to note that we don't put a return statement here since this will mean that we're returning from the main
function. We want to return the value from this if else statement. So, need to omit the return statement and then simply return the value without any semicolons. Let's print this value out. Z is equal to Z. Execute the code and get Z is equal to 1. Since we started out with x equal to 10 which is a positive number so it returned this part of the code. There are several ways to write loops in rust. The simplest way is to use the keyword loop followed by curly braces. And inside here we'll write the code that is executed for each iteration. For example, let's say print len and then loop. Execute the code and the loop never ends. To fix this let's create a
counter. Let's say let mute i equals to zero. initialize to zero and then inside the loop we'll increment this counter by one. We'll exit the loop if this counter is greater than five. To exit the loop we'll use the keyword break and for each iteration we'll print the current count. Execute the code again and this time the loop runs from zero to five. We can write the same code using a while loop while I is less than or equal to five. And then we'll also reinitialize this mutable variable called I the counter for the loop. For each iteration, we'll print the current count and then increment the count by one. For the message to print, I'll change this to while loop. Execute the code and the
while loop runs from 0 to 5. So this code over here and the while loop does the same thing. Now, if you wanted to write this using a for loop syntax, here is how you do it. Let's say four i in zero going all the way up to five. But here the five will not be included and we want to include five. So this number should be a six. Print len let's say for loop and then print the current count. Execute the code and we get for loop running from 0 to 5. So now we seen three ways to run our loop. You can also loop through arrays and vectors. So let's say that we have array. Let equals let's say 1 2 3 4 and 5. So there are several ways to loop through an array and vectors. The first way is to first
get the length of this array that n. Remember length in arrays and vectors have the data type u size r dot bank and then run a loop for i and zero to n print ln r of i. Execute the code and we get r running from zero to four. If you wanted to access the element of the array elements for each iteration, we can do r of i. Execute the code again and we get the elements of the array printed out. We can also directly use arrays and vectors inside the for loop. So say four, let's get the number n in r and then print this out. Print len.
Let's say r and then print here n. Execute the code again and again we get the same result as the last for loop. Elements of the array being printed out 1 to 5. We can also use the same syntax for vectors. So let's say we have a vector let b is equal to be with elements. Let's say 10, 20, 30, 40, and 50. And then we'll use this syntax to run a loop for vectors for n inside the vector B. For each iteration, print the element inside the vector. Execute the code and we get the vector elements being printed out 10, 20, 30, 40, and 50. Now, here's a surprising part. Once you run this for loop, if you try to run
it again, the code will not compile. And to show you this, let's try running the code. and you'll see that we get an error. The reason why we cannot run a for loop for vectors using this syntax has to do with the borrowing rules and the ownership that is unique to Rust. I'll explain about ownership and borrowing rules later in the course. So give you a quick explanation of what's going on here. When we use this syntax, it's actually doing this code over here into it. It's turning the vector into a iterator and the vector is being owned by the for loop. So you can only run it once. The for loop takes ownership of the vector B. So once the for loop is done executing, this vector is dropped.
That's essentially what is happening. If you wanted to run the for loop twice in a row, then you'll need to explicitly call it. This will turn the vector also into an iterator without the for loop taking ownership of the vector. So if you wanted to run this for loop twice for vectors, you'll have to call it and then call vector.iter it again. Execute the code and we can see that the vector elements printed out twice. Again, I'll explain about ownership and borrowing rules later in the course. For now, just remember that if you wanted to loop through a vector using the for loop syntax multiple times, then you'll need to call it on the vector. And finally, I'm going to show you an example of
using this loop syntax to return a value from a loop. So, first I'll copy this and then we'll go all the way down and then paste this code. So this will be an example of returning a value from a loop. Remember when we looked at example for if and else statements in rust we can return a value from the statement. This is also true for loops. We can return a value from a loop. For example, over here the loop exits. So over here after the break statement we can return something. For example, let's return a string literal. Let's say loop ends here. And then let's assign this to a variable. that Z is equal to. Here the type of the value that is being returned
is a string literal. And then finally, when you return a value, you'll need to put a semicolon at the end of the curly braces. Let's print this value G out. Let's say print ln loop returns Z. Execute the code and we get the value loop returns loop ends here. So these are some examples of working with loops in Rust. Match is a syntax that will allow us to execute code depending on the value that was matched. For example, let's say that we have a variable equal to one. To match this value and then execute a conditional statement, we'll start with the keyword match followed by the variable that we want to match, match x. And inside here, we'll write our conditional statements. For example,
if x is equal to one, let's print ln one, a string one. If you wanted to match x equal to two, then let's print two over here and then match for three as well. Now, if you try to execute the code, you'll get a compilation error saying that not all possible value of x was matched. Here, the value of x is a i32. But for the conditional statement, we're only matching for the case of x= 1, 2, or 3. We need to match for all possible cases of the value for x. We don't want to list out all the possible values for x. So, here we can use the wild card. If x does not match one, two or three then we can put a catch all statement with the underscore. Let's say print ln others. So now this match
statement will execute print ln for one. If x is equal to 1. If x is equal to two, it will print out two. If x is equal to three, it will print out three. Otherwise, it will execute this part of the code which will print others. Now consider the case that for numbers 1 2 and three maybe we wanted to say 1 or two or three. So this is the same case for numbers two and three. So this is one way of doing it. Another shorter way of writing the same thing is to say 1 or two or three. So these two code do exactly the same thing. Okay. What if you wanted to match a range of number?
Here we're matching 1 2 or three. What if you wanted to match let's say from 1 to 10. Of course, you can list out all possible values from 1 to 10 saying 4 5 and so on. However, there's a shorter way of writing the same syntax by saying 1 dot dot equals 10. Here the values will be matched from 1 to 10. So notice that we put a equal sign here. This says that we're matching from 1 to 10. If we remove this, it will match from 1 up to 10 but excluding 10. So for example, let's say that X is equal to 10. In this case, X will not match the first statement. So it should print out the value others. Let's execute the code.
And we get others printed out. Now going back, let's change this to a equal sign. So this statement will now execute for numbers from 1 up to 10 and including 10. Execute the code again and we get 1 to 10. What if you wanted to know the value that was matched? For example, here it's saying if x is between 1 to 10, then execute this code. But what is the actual number that was matched? In this case, we can do the at syntax. I this is the name of the variable that we assign it to and then say at if x matches one of the values from 1 up to 10, then assign that value to the variable i. Let's print this out. Put a i over here. Execute the code again. And this time we get it matched from 1 to 10
and the actual number was 10. So far we saw examples of matching a simple variable. But the two most common cases for using the match statement is with the option and the result enums. So let's look at example of a option. Let's say that x of type option of i32 is equal to sum with some number. Let's put in a nine over here. Again we'll use a match statement. So there are two possible values for an option. sum with some kind of value inside and none in the first case let's print this inner value print ln option is some value inside otherwise let's print ln none execute the code and we get option is nine now if we change this to a none
and execute the code again this time we get a none printed out so this is an example of an option matching with an option next let's take a look at matching with a result type. Let's say let rest up type result. The result type takes on two other types. The type that is returned in the okay case and the type that is returned when there was an error. For the okay case, let's put in a number I32 again. And for the error case, let's put in a string. This is equal to let's start with the okay case with some number 1 2 3. And then we'll use the match statement again. Match rest. There are two possible cases for the match case. Okay. with some kind of value inside. In this case, let's print
ln okay with the value inside. And then the other case is air. Air with some air inside. Let's say print len air. And then we'll print out the air as well. Let's execute the code. And we get okay with the inner value 1 2 3. Now next let's look at example when the result type returns air with some string. Let's say something failed fail. string again execute the code and this time we get the message error failed. So these are some examples of using the match syntax with the nums option and result. Now in the same way that for if statement and for loop statement we can return values. Match statements can also be used to return values. So for
example, let's assign the return value of a match to a variable called Z. We'll return a type of U32 and then say equals match. Let's match. How about an option? So, we'll take this example. Let X equals an option of type I32 and then put it in here. Since our option is a I32, actually let's return I32. And then say sum with some kind of value, we'll return the value. Otherwise, if it is a none, we'll return a default. Let's return a zero. And then let's print this result out. Print ln match returned Z.
execute the code and we get match return the number nine. How about if the option is a none? So we'll reassign this variable x to the value none and then execute the code and this time we get match return is a zero. So the part of the code that executed this time was it matched this part of the code none. So it returned a zero. So this is an example of using the match statement to return a value. We looked at how to use the match statement to match with the option type. Let's say that we match a option type and if there is some kind of value then we print out the value and if it is a none then we don't do anything.
We're only interested in the case when the option is a sum with a value inside. In this case we can use the if let statement to make this code shorter. So here is how you would write the same exact code using the if let statement. If let sum val is equal to x here it's matching this variable x and if this x is a sum with value then let's print this exact same code. These two code do exactly the same thing and this works because in the case that the variable x is none we're not doing anything. Execute the code and we get option is n.
So when you have a match statement and you only need to consider one case and for the rest of the case you're not interested then you can rewrite the same match statement using the if statement. Here I showed you example using an option but you could have wrote a similar code for the result type as well. Stack and heap are two regions of memory used for different purposes. They become quite important to understand Rust's unique concepts called ownership and borrowing rules. These are two concepts that ensure Rust memory safety. So I'm going to first explain what stack and heaps are and then give you some examples. Stack is a region in memory where data of fixed size known net compile time are stored. Data types like U32, I32 and boolean where the size of that data is known at compile time will
be stored on the stack. Data access on the stack is fast and way that the data is stored is fast and first out. If you store the data A, B, and then C, the first data that you can get out is C. If you wanted to get the data A, you first have to remove C and then B and then access A. And now let's move on to heap. Heap stores data of unknown size at compile time. For example, data type like strings and vectors can grow and shrink at runtime. So these will be stored on the heap. Data access on the heap is slower compared to stack. The memory safety for data on the heap is enforced to Rust ownership and borrowing rules. Let's look at some examples of data on the stack and data on the heap.
Examples of data on the stack are data where the size is known at compile time. For example, I32. The size of i32 is known at compile time. Another examples are fixed size arrays. The size of these data are also known at compile time. In the first example, an i32 will take up 32 bits. In the second example, we know that size of each element i32 will take up 32 bits. And this array has a fixed size of 10. So this will take up 10 * 32 bits 320 bits. In both cases, the compiler can calculate the size of the data. Hence, they will be stored on the stack. Next, let's take a look at some data types that are stored on the heap.
One example is a string. The reason why the string is stored on the heap is because the size of the string can change when the program executes. Hence, the size of the string is not known at compile time. For example, we can change this string into a mutable string and then at runtime we can add more strings to this. For the same reason, a vector will be stored on the heap. If we have a vector and then at runtime we add some values to this, then the size of the vector changes at runtime. Hence, the size of the vector is not known at compile time. These two are examples of data that are stored on the heap. And finally, we can force any data type to be stored on the heap. Here we said that the i32 the variable x is stored on the stack since the size of a i32 is known
at compile time. But we can tell Russ to store it on the heap by using a box. The way we do this let's say box is equal to box colon new and then inside here we'll put in our data. Let's put in i32. 1 i32. This will put the value i32 on the heap. If you did not wrap this in a box, then the i32 will be stored on the stack. So these are some examples of data types that are stored on the stack and on the heap. Ownership rules in Rust ensure memory safety. The rules are each value has a owner. There can only be one owner at a time. When the owner goes out of scope, the value will be dropped. Let me explain these rules one at a time.
I'll start with the first rule. Each value has a owner. If he had a variable called S, then the owner of this string S is S. If you had a variable called I which is equal to one, then the owner of this value is the variable I. Each data has a owner. The string rust is owned by the variable S and the value one is owned by the variable I. Let's move on to the second rule of ownership. There can only be one owner at a time. Again, we'll start with a string S having the value dog. The owner of this value dog is the variable S. Now, if you reassign this variable S to another variable, let's call it S1, then the owner of the string dog, will be transferred over to the variable S1. If we do this again, assign the variable S1 to a new variable
called S2, then the owner of the string dog will again be transferred from S1 over to S2. Since there can only be one owner at a time and currently the owner of the string dog is owned by S2, we can print S2 out. Execute the code and we see dog printed out to the terminal. However, if you try to print the variable S, this will not compile. Try to execute the code again and we see that the code does not compile. This is because the owner of the string dog is now owned by the variable S2. So, the variable S is invalid. It is no longer the owner of this string. But the ownership rule says that there can only be one owner at a time. Therefore, once we transfer the ownership of a value to another variable, we can no longer
reference that variable. The final rule says that when the owner goes out of scope, the value will be dropped. Value being dropped means that you can no longer reference that value. The simplest example of this rule is the following. Consider that we have a string called s having the value cat. We use this variable s inside another scope. We can introduce a new scope by using this variable s inside the curly braces. For example, this curly braces can be a if statement or a loop or simply a curly braces. This introduces a new scope. When we use the variable inside a new scope, the variable moves into that scope. So once the code inside this scope is done executing, this variable will be dropped. In this simple example, the scope ends here. So the
variable s will be dropped at the end of the curly braces. If you try to print variable S after it is dropped, the code will not compile. Try to execute the code and again we see a compilation error. This is because the variable S is used inside the scope and at the end of the curly braces, this variable S is dropped. So outside of the curly braces, we can no longer use this variable S. Let's look at example of applying rule number three with rule number two. To begin with, I'll copy this code and then paste it here. So from rule number two, remember that when we reassign a variable to another variable, the owner of the value transfers over to the new variable. So inside here, let's create another variable. Let's say that S1 is equal to S. So the owner of the value
cat is transferred over to the variable S1. At the end of the curly braces, the variable S1 is dropped. Since the ownership of S has transferred over to S1 and then S1 is dropped at the end of the curly braces. If you try to print the variable S outside of the curly braces again this will not compile. Try to execute the code again and we still get a compilation error for the same reason that the owner of the string cat is dropped at the end of this curly braces. Now we can take this simple example and then apply it to functions. So far for these curly braces, we imagine that this is like a if statement or a loop or simply curly braces that introduces a new scope. But the same reasoning applies when we apply it to a
function. So imagine that this is some kind of function that takes in a string as input. You'll see that the same thing applies inside the function. It introduces a new scope. The ownership of the input will be transferred over into the function. And when the function is done executing, that input will be dropped. Let me show you an example. Here's a function take. It takes in a string as input and then it prints the string. When this function is called, the ownership of string guess will transfer into the function. When the function is done executing, this string guess will be dropped. So back inside the main function, let's say that we have a string guess and then we call the function take guess. When we call this function, the ownership of this string guess transfer over to inside the
function take. After the function take is done executing this string s will be dropped. So back inside the main function. If you try to print the string s after calling the function take the code will not compile. Try to execute the code again and you can see that the code does not compile for the same reason that the ownership has transferred over to the scope of the function and when the function is done executing the variable is dropped. So these are three examples of rule number three. When the owner goes out of scope the value will be dropped. Now, you can ignore the three rules that I explained above for data types that implement the copy trait. Basically, datas that can be copied. When a data is reassigned to a new variable or it is used inside a new scope, instead of the ownership
transferring over, it copies the value. So, we don't have to worry about ownership. Consider a simple example about an I32. The data type I32 implement the copy trait. They can be copied by values. Here, the owner of the value one is the variable I. If we reassign it to a variable called I1, then instead of the ownership transferring over, the value is copied over. So both I and I1 will hold the value for one. And then if we reassign I1 to I2, then the value that is stored in I1 will again be copied over to I2. In the case for strings that doesn't implement the copy trait, when we pass the string into the function take, the function take took ownership of the string. So afterwards, we couldn't print it inside the main function. However,
for data types that implement the copy trait, we can pass the data into the function without worrying about ownership and then use it inside the main function again. For example, let's say that I have a function called copy which takes in I32 as input and then prints out this value. This value is dropped at the end of the function. Now, let's call the copy function passing in the variable I. When we pass the variable I as input, the value for I will be copied as input. So, the ownership of I will stay inside the main function. So after calling the function copy, we can still use this variable I execute the code and the code compiles and executes without any error. So in summary, the ownership rule says that each value has an owner. There can only be one owner at a time. When the owner
goes out of scope, the value will be dropped. However, these three rules can be ignored for data type that implement the copy trait. When we talked about ownership in Rust, we had a particular example where we created a string s and then passed it into a function called take. The function take will take a string as input and simply print it out. Now since when we pass a string into another function, the function takes ownership of the string. If you try to print this string inside the main function, we saw that the code did not compile. Of course, not being able to use the string as inside the main function is a little bit impractical. So we have another set of rules in Rust called the borrowing rules which will allow us to pass the string into another function without that function taking ownership of the string. Here I'm using
a string as example but this transfer of ownership applies to all data types that don't implement the copy trait. So what does it mean to borrow data in Rust and how can we modify this function so that we can use the borrowing rules instead of transferring ownership into the function. Let me first explain what the borrowing rules are. Borrowing means that it temporarily uses a value without taking ownership. In the example above, inside this function, we pass a string as a input. It is taking ownership. By using borrowing rules, this function will be able to use the value without taking ownership. So that inside the main function, we'll be able to print this string guess after calling the function take. To use a borrowing rule for the data that we're concerned with, we need to create a reference. The
reference can either be mutable or immutable. When we create a reference, the borrowing rule applies and the data doesn't move ownership. Now there are immutable reference and mutable reference. For immutable reference any number of read only access to a value is allowed. For mutable reference this means that we have a reference to a variable that we can mutate. Therefore only one read and write access to a value at a time can be created. Only one mutable reference can be created. We can't have both immutable and immutable reference created at the same time. we either have immutable references or a mutable reference and not simultaneously. And finally, references must not outlive the value. If we
reference a value, that value must live longer than the reference. So now let's take a look at some of these examples. Let's begin by creating a immutable and immutable reference. Let's say that we have a string called s. To create a immutable reference, all we have to do is create a reference to the variable by putting the amp sign before the variable. Here we're creating the immutable reference and then assigning it to a variable named s1. We can create multiple immutable reference. So on the next line we create another immutable reference called s_ub_2. If you assign s2 to another variable called s3 since s_ub_2 is a reference to the variable s, s3 will also be a reference to the variable s. Here we're creating the immutable reference. S1, sub_2, and s3
all have read only access to s. This is an example of how to create immutable references. Let's now compare this with ownership. For example, if we create a string called S and then assign this S to a new variable called S1, then the ownership of the string dog moves over to S1. So now on the next line, we cannot assign S to a new variable called S2. All we can do is transfer the ownership of the string dog from S1 over to S2. There can only be one owner at a time. However, for borrowing, we can create multiple immutable references.
Next, let's take a look at mutable borrows. We'll start by initializing a mutable string. To create a mutable reference, we will do amp% sign followed by the keyword mute on the variable that we want to create a mutable reference. In this example, we assign this to a variable called s1. So now s1 has read and write access to s. Since s1 has read and write access, we can modify the original string s. For example, pushing the emoji of a crab. Now we can only have one mutable reference existing at a time. So if you try to create another mutable reference s2 then this code will not compile. However, if you move this line of code to after modifying the string guess using the reference s1 then this code will compile. For example,
after pushing the emoji of a crab into the string s using the reference s1 and then we create a new mutable reference called s2 and then do the same thing except here push the string using the mutable reference s2 then this code will compile. Okay, so why does this work? Let's check back on the rule for mutable borrows. For mutable reference, we can only have one read and write access to a value at a time. So if we apply this rule, it looks like we have two mutable references s_ub_1 and sub_2. However, the important phrase here is at a time.
What's going on here is first we create a mutable reference s1 and then modify the string s. Afterwards, s1 is no longer used. So this s1 doesn't have to exist anymore. And then we create a new mutable reference called s2. So there are two mutable references here. However, there's only one mutable reference that exists at a time. Therefore, this code will compile again. However, if you move this up over here, then the code will no longer compile because now we have two immutable references that are living simultaneously. Let's move on to the next row. Either immutable or immutable borrow, but not both simultaneously. So, going back to this example, let's create some immutable references. Now, we have
two immutable references, S1 and S2. What does it mean to not be able to create both immutable and mutable references simultaneously? Well, what if we created a mutable reference over here? Let's call this S3. Since we're creating a mutable reference, I'll also prefix this string S with the keyword mute. So now we have two immutable references and a mutable reference existing at the same time. to make sure that all of these references live long enough. If you try to print S1 and then try to modify the original string S using the mutable reference S3, this code will not compile. To show you this, we'll try to execute the code and the code fails to compile. This is because we have an immutable reference and a
mutable reference existing simultaneously. Since both immutable and immutable reference cannot exist simultaneously, if we comment out the code where it creates a mutable reference and then try to execute the code again. And now the code compiles and also executes. Let's move on to the final rule. Reference must not outlive the value. So what does this mean? First of all, how can we create a reference that will outlive the value? Well, let's start by creating a reference. So we'll start with string S and then create a immutable reference S1. So now how can we create a situation where the reference will outlive the value? Well, we can apply the ownership rule. For example, let's say that S2 is equal to S
inside a new scope. So what's going to happen here is the ownership of S will transfer over to the variable S2. At the end of this scope, S2 will be dropped. So this value will no longer be valid. However, the reference to the string S sub S S1 divs outside of this scope. Over here, we will have s_ub_2 and s no longer exist. However, we still have S1 that references the variable S. So, this is a situation where the reference outlives the value. To show you that this code does not compile, we'll try to print the reference S1 over here and then try to execute the code. And you can see that the code does not compile.
Again, this is because we're in a situation where the reference S1 lives longer than the value S. Another example of a reference outliving the value is having a function that takes in string s and then trying to return the reference to the string s. When this function is called, the ownership of string s moves inside the function. When the function s is done executing, this string s will be dropped. However, here we're returning a reference to a string. Therefore, we're returning a reference, but the value has already been dropped. Hence, this is an example of a reference outliving the value. This code will also not compile.
So now that we've seen examples of the borrowing rules, let's go back to our original function. So how can we convert this function so that inside the function we print the string without taking ownership of the string. I'll first copy this function and then I'll name this as borrow. Here we'll only need a read access. So I'll turn this into a reference to a string and then we'll print borrow string. Now let's call this inside the main function. Here we initialize a string s and then let's call the function borrow. We need to pass in a reference to a string. So I'll put an amperson sign before the variable s. And then after we call the function borrow, let's try to print the original string s. If the code compiles and
executes, this shows that the string s ownership of string did not transfer into the function borrow. So that we can still call it inside the main function. Execute the code and we get borrow rust. So in summary, we can borrow values by creating a reference. When we create a reference, it doesn't take ownership. It simply borrows the value. We can create either an immutable reference or a mutable reference, but not both simultaneously. We can create multiple immutable references simultaneously. However, we can only have one mutable reference at a time. When something goes wrong inside your Rust program, there are several ways to handle the error.
The simplest way is to use the panic macro and crash the program when something goes wrong. If you try to access a vector or array with a index out- of-bounds, then this will also panic. One way to graciously handle the index out- of-bounds error is to call the method get on a vector. If the index is a valid index, the function get will return sum with the value inside. If the index is out of bounds, it would return a none. Another way to handle an error is to use the result type. The result type takes two other types. The type that is returned when everything was successful and the type that is returned when there was an error. The result enum returns a okay with the type t when something was successful and it returns a error with the type e when there was an error. Let's look at example of when
the result type will be useful. Consider that we divide two numbers x and y. When y is equal to zero, this will panic. This is a division by zero. Let q be a result type of i32 and a string. When the division of x / y is successful, we'll return an i32. If there is an error, then we'll return the string with the error message. Here we'll use the if else statement to return the value. If y is not equal to zero, we'll simply divide x / y wrapped in the okay. Otherwise we know that y is equal to zero. So we'll return the error message div by zero. Now once we have this variable called q, we can use the pattern matching to see what the value is. The two possible cases are okay and error. In the case of okay, the result
of x / y will be stored. In the case of an error, there will be a error string. Let's also print this out. execute the code. Since by is equal to zero, we get the error message did by zero. This is an example of how we use the result type to turn a program that panics into a program that returns a result and the program no longer panics. We can make one more improvement to this code. Instead of returning a string for air, we can return an enum. Let's define an enum called math air. The math air enum can have two values div by zero and other. When we encounter a division by zero, let's return math air div by zero enum. Now to print this enum, let's attach the derived debug attribute onto this enum. And finally, let's move this
enum up to the top of the file outside of the main function. Execute the code again, and this time we get the enum printed out, the enum div by zero. In summary, we saw three ways to handle errors. The first way is to use the panic macro to crash the program when something goes wrong. The other way is to use the option enum. The option enum is used to express the presence or the absence of a value. This works well when you want to access vector or array elements. The last way to handle errors is to use the result enum. When something is successful, we return okay with the value inside. When something failed, we return error with the error type wrapped inside. Sometimes you need to get the value out of option. For example, let's say that we have a
variable called x of the type option i32. We do a pattern match and if there's sum with a value we return this value as i32. If it is a none then we'll panic this program. Now this is a common pattern in rust that there are two functions that we can use to write the same code. The two functions are called on wrap and expect. The function on wrap is available on options and results. For an option it will return the inner value when it's a sum. For result type it will return the inner value when it is a okay. This function unwrap will panic when the option that is unwrapped is none. And for result types, the function unwrap will panic when the variable contains an error. In this example, the variable x hold the inner value called three. So when we call the function
unwrap, we get the inner value back. Let's print this out. Execute the code and we get the inner value printed out three. The function unwrap also works on result. Let me show you example. I'll copy this code and then paste it here. And then we'll turn this into a result of the type i32 and a string. The variable x will be let's say okay of three. And then we'll do a pattern match. If okay, we'll return the value. Otherwise, if it is a error with some error message, we'll print this error out. So imagine that we have this code variable x of the type result. We match variable x. If it is a okay, we want the inner value out. If it is an error, then we want to panic here. In this case again we can use the unwrap function.
Let's execute the code and for the result again we get an inner value of three. Now what happens when instead of a okay we return an air. For example let's copy this variable paste it here and change the okay to air. Air dot to string. Execute the code again. And this time the code panics since we called unwrap but we cannot unwrap a air type. You can see that these two code do exactly the same thing. When we call the function unwrapped and it contained okay with a value, we got the value back.
When the result typos an error, the code panicked and this is the same as the code above. This behavior also works with an option when the variable X is a none. To show you this, I'll copy the variable and then paste it here. And then we'll define it as a none. When the function unwrap is called, the code will panic over here. Execute the code. You can see that the main function panicked since we called unwrap but it returned a none. A similar function to unwrap is expect. Expect behaves the same as unwrap. The only difference is you can put in a custom air to be printed out when the code panics. Now let's create a code that behaves similar to this code above using expect. To use expect, we'll call x.exect.
The function expect exists on both options and results. Inside the parenthesis, we'll put our custom error message. For example, something failed. Then we'll comment out this code above. Execute the code and we see that the main function panic with the message something failed. In summary, unwrap and expect are two functions that you can call to get the inner value from a result or an option. The question operator is something that you can use with functions that return a result to write shorter code. For example, let's say that we have two functions f_sub_1 and f_sub_2. Both of them return a result of type U32 and a string. In this example, we'll keep it simple and then print the function that is called and then return the okay with the value
inside. F_sub_1 returns one and f_sub_2 returns two. Now consider a third function where inside this function we call the function f_sub_1 and f_sub_2. This function will get the result from f_sub_1 and f_sub_2. It will add the results and then return the sum. We can pattern match on the first result to get the value when the result wasn't okay. In the okay case, we'll simply get the value out. In the error case, we'll return an error. We do the same for result two. If result two is okay, then we get the inner value. Otherwise, if it is an error, then we return the error. Here, if both of the results return okay, we add the values and return the sum wrapped in okay. Now, we can write the same function shorter using the question mark operator. I'll begin by copying this code and then we'll paste
it here. Let's rename this function to f_sub_1 f_sub_2 question. The return type will be the same. If it is okay, it will return U32. If there was an error, it will return a string. To use the question operator, we append the question mark to the function that we're calling. In the case of a okay, this will return the inner value that is stored than okay. In the case of a error, the execution will end here and it will return the error. So these two code do the same thing pattern matching or we can simply append a question mark. We do the same for the function call to F2. In the okay case it will return the inner value of the okay. In the error case the error will be returned here. So now this is our final code. Call F1 with the question mark. Get the inner value.
Call F2 with the question mark. Get the inner value. Finally add the two results up and then return it with a okay. Notice how much shorter code this is compared to this code over here. And finally let's call this function inside the main function. Call the function f1 f_sub_2 question. get the result and then print it out. Execute the code and we get okay wrapped in three. So in summary, you can make your code shorter by using the question mark operator when the function returns a result. Modules are the way to organize your code in Rust. Here I have a function called print called inside the main function.
Let's turn this into a module. Let's define a module called my. This will be my module. And then inside it we'll put this function inside. This creates a module called my and it has a function called print. How do we call this? Inside the main function, we do my colon then call the function print. However, we will not be able to call the function print. The function print is a private function inside the my module. It can only be called inside this module. To make this public so that we can call inside the main function, we need to prefix this with the keyword pub. So this will make this print function a public function. In contrast, if you wanted a private function inside the my
module, then you will remove the pop keyword. For example, let's call this private print. This is a private function that can only be called inside this module. The function print is a function that can be called outside of the module. In our example, we call inside the main function. Let's execute the code and we get the string rust printed out. Now, inside the module, we can also nest other modules. For example, let's say that we have a module called 8. And then inside this module, let's also declare another function. The same function called print. To not to confuse oursel, let's print a different message. Let's say a. Now, how do we call the function print inside module A.
Currently, this says that the function print inside module A can be called inside my module. So, for example, inside here, we can call a colon print. How about inside the main function? How can we call the function print inside module a inside the main function? We can do this by saying my colon a colon print. Now like in the first example where we had to prefix this function print with the pub keyword, we also need to make this module a public module by prefixing it with the pub keyword. Run the code again and this time we see the string a printed out which comes from the module a inside the function print.
We can also define strrus inside a module. So inside mod a let's declare a strct. Let's say strruct s. It will have two fields name string and id u32. Now for us to be able to use this strct s inside the main function. Again we'll need to prefix this strct with the pub keyword. Now we can access the strruct s inside the main function. For example, let's say let s equals my col a col s. However, the strruct fields id and name are both private. They can only be accessed inside module a. To make these two fields public, we need to prefix it with the keyword pub. And now inside the
main function, we'll be able to initialize this strct. For example, id zero and name. Let's say rust.2 to a string. But what if we remove one of the prefixes? So now ID is a public field. So we'll be able to access it inside the main function. However, name is not a public field. So we won't be able to access it inside the main function. To show you this, let's try running the code again. And you'll see that the code does not compile. Name is a private field. How can we keep the strct like this so that ID is public but name is private but be able to initialize a strct? One way you can do this is to create a new function, a public function that will initialize a strct s. Let's say this function is called build. It
will take an id of u32 and then it will return a strct test. Say id will come from the input for the name. Let's keep it a empty string dot to string. The function build is a public function. So over here we can call this. You can say my colon a colon build pass in id let's say one run the code again and now the code runs without any errors again next I'll explain about the keyword called super imagine that we have another model let's call this fu inside it it has a public function called print so we have two modules fu and my inside the module my how can we call the function print
that is inside module fu so let's create a function pop fn print inside here. What we want to do is say fu colon print and call this function. To be able to do this, we first need to import fu inside my module. To do this, we say use super colu. The super keyword tells Russ to look for the module one above the current module. To visualize this, let's say that this is the file. This is the file. In this file we have two modules food and my and inside my we have module A. So inside my we have module A. To access the food
module inside the my module we need to go one above so that the context will be inside this file. At this level Rust can recognize that there's a food module. So that is what super means. It means to look for the module one above the current module. So we go one above and from one above we can see that there's a fu module. So Russ knows that this fu module refers to this fu module. Now let's look at example. What if we wanted to call the fu module inside module a. So what we want to do is call this function inside module A. Going back up to the diagram relative from module A, we need to go up once twice to be able to see module food. So inside module A,
there's several ways to be able to call FU. This code over here says to look for the module fu, one above module A. One above module A will land us in module M. Inside module my, we've imported the food module. So back inside module A, it knows where to look for module fu. How about if we comment this out and also comment this code out. Now inside module A when it says to look for module fu one above this module it no longer can see module fu. So to fix this we need to go one more above. We need to say super col super food. Let's see what it does.
Super means go one above and then super again means go one above again. Going up twice will land us over here where rust can see that fu module refers to this module. So that is an example of super. Let's try calling the function print fu inside moddio inside the main function. Let's say my col a print fu. Execute the code and we get the string fu printed out. So these were some examples of how to use the module to organize your code and how to use the keyword super to import modules. The next thing I want to do is organize these modules into separate files. Currently, this file is located under examples and the name of
the file is called mods.rs. To put these into separate files inside the source folder, you'll create a new file called div.rs. And then going back to the mods file, let's cut all of this code out, all of the code that declares modules, and then paste it in here. Now remember that to make the module public so that we'll be able to use inside the main function, we need to prefix the module with the pub keyword. So I'll add the pub keyword to all of the modules over here. Now going back to the mods RS file, the file that we're working with currently is lib. RS and the file where we pulled out the modules mods. RS inside examples. So inside here we need to import the
modules mic. So you might have already guessed that we need to use the keyword use to import the module. But now for files inside examples we need to first import the package. The name of this package is declared inside cargo.tl. The name of this package is called hello rust. So back inside the mods. RS we need to say hello rust. And now we can import the module my let's try executing the code again. Execute the code. And the code still works. So going back, if you wanted to also import the module fu, then you would put curly braces and then type fu comma and then closing curly braces. This will import the modules fu and my but. But for this example, we're only using the my module. Let's now turn
our attention to lib. RS inside here, we still have multiple modules inside a single file. How do we split all of these modules into separate files and folders? We'll begin by splitting the fu module and the my module into separate files. So inside source, we'll create two files. FUR rs and myrr rs. Going back to the lib rs. Let's start with the fu module. I'll cut this code and then paste it inside the file flu. RS. Okay. And then going back inside lib. RS. I'll cut all of the code inside my module.
And then inside my RS, I'll paste this code. Okay. And then going back to lib. RS, I'll remove this curly braces and then end with a semicolon. So now we successfully split our modules into two separate files. Fu rs and my. RS. Our work for fu. RS is done. It only has a single function. But for my. RS, it is a nested module. Inside the module my, it has another module called a. So now let's split this module into a separate file. To do this, we'll first create a folder inside source called Mike. This is the same name as the module Mike. And inside here, we'll create two files. One called mod. RS and another called a. RS.
Inside mod. RS, we'll declare the modules of mod A. And then inside A. RS, we'll put our code that is inside here. Copy all of this code and then paste it inside A. RS. Now going back to my rs, let's clean this code up. We no longer need this code since we moved it over to mod rs and a rs. We still have two more functions over here. We'll cut this code out. And then inside mod rs. We'll paste it. And finally, we can remove this file my rs. You can still execute the same code mod. RS. Execute the code and the code still compiles and works without any errors. So to recap what we did for
the top level modules, we declared it inside a file called lib. RS. The modules are food and my food is a small module which only has a single function. So we created a new file called food.rs. However, my is a nested module with more code inside. So we made a folder called my. To declare that this is also a module, we need to create a special file called mod. RS. Inside here we declared some functions print and private print. And to declare that this module my module has another module called a. We declare that there's a module by saying pub mod a. And then inside a. RS we put in our code for mod my col. Back inside the mod. RS we can still call the same
functions without changing any code. And finally going back to a. RS. Remember that the keyword super means to look for a module one above the current module. Now you can sort of visualize this by looking at the file structure. The FU module is super super. So go to above. If you go one up, this will land us in mod. RS since mod a is declared as a module inside my. And then if you go one more up, it will land us in lib. RS where the module my is declared. From here we can see that the module fu is also declared. So back inside a rs it knows where to look for the fu module.
It sees that it needs to access this module fu that is declared inside lib. RS. So this was an example of how to split modules into separate files. Generic types are types that are parameterized by other types. We've already seen three examples of generic types. These are option, result, and vectors. Let's see how these are generic types. How are they actually defined? For an option, this will be a enum. The name of the enum is an option. This option is a generic type. So it is parameterized by another type. We declare that this enum is a generic type like this. Here the capital T is a type placeholder. So inside here you will have your concrete type. When we create this enum, for example, an option of U32
will have a U32 over here. An option of boolean will have a boolean over here and so on. This is a type placeholder. We'll call this T. An option can take on two possible values. Sum with a value of the type T inside or none. So this is how an option is defined. It is a generic type with the type placeholder T. Let's look at example of how this would turn into a concrete type. For example, if you had again U32, then this T will be replaced by U32 and sum will contain U32. This will mean that if the value exists, it will return sum with the value of the type U32 inside.
Otherwise, it will return a none. Another example, let's say we have a string. Then this will mean that if there's a value, then it will be sum with a string inside otherwise it'll be a none. Of course, this logic works for many data types. So, we replace it with a type placeholder. This is an example of a generic data type. Now, let's look at example of a generic data type for result. What does it look like? How is it defined? Result is also our enum result. Inside here, we put in our type placeholders. Recall that a result takes on two other types. The type that is returned on a successful case and a type that is returned on the error case. Now, notice that here we define two type placeholders T and E. How are they used?
In the okay case, we return a type of T wrapped in okay. In the error case, we return the enum value of error with the type E inside. So this is how an option and a result is defined. I'm going to comment this out so that the code still compiles. Since both of these enums are already defined inside Rust. How about a vector? Well, inside the main function, when we create a vector, let's say that B of the type bec. And then inside here, we'll put our concrete type. And this will be equal to beck. And then inside here we'll put in our values for each element of the vector. For example, if we put in U32, for example, 1 U32 and 2 and 3. The vector elements are of
the type U32. So this type placeholder T will be a U32. If you change this to a I32, then this U32 over here will be a I32. You might be asking why are generics useful? Generic types are useful when you want to reuse some kind of data structure or logic. But that logic or the data structure doesn't really depend on the type that it stores. This is the case for an option result and also for vectors. The data that it stores doesn't really matter. For a vector, we want to store elements of the same type and also be able to add and remove elements from a vector. The type that it holds doesn't really matter. It should work for booleans, U32, i32, strings and so on. So this is
where generic types are useful. Now let's look at two more examples. How to define our own generic type and how do we use generic types inside a function. Let's start by defining a generic data type. So let's say that we have a strct called point. Inside this point, we'll store two values. The coordinate x we'll store as i32 and the coordinate y we'll also store as i32. This represent a point in 2D space. To initialize this, let's say that P is equal to the type will be point and this is equal to let's say point X0 and Y0. Now we could have also created a point where this type is a U32 and we could have also created a point where both of these types are
floats. So maybe we wanted to create a point with U32 and maybe we wanted to create a point with F32. Currently, we can only create a point of the type I32. So, how can we turn this data structure into a generic data type? Looking from the examples above, we sort of have an idea of how to declare generic types. For the strct, we would declare the type placeholder T. And then let's replace X with the type T and Y with the type D. Now, going to the main function, we also need to update this. Looking at the syntax for vector, we need to put in a type placehold here. Here we'll need to put in our concrete type. If you wanted to create a point where the X and the Y
are I32, here we'll put a I32. If you wanted to create a point with U32, you will replace this I32 with a U32. And finally, if you wanted to create a point of the type float F32, then we will replace the U32 with F32. And also, we'll need to update the data that is stored. We'll need to say 0.0 to make sure that this is a float. So this is an example of how to create a generic data type. Now let's look at a simple example of a function that operates on generic types. So let's begin by having a function that takes in let's say a of the type u32 and b of the type u32. And this function will swap the values for a
and b. The return type will be u32 and u32. All we do here is simply switch a and b. So if you pass a and b then this function is going to give you back b and a. Let's call this function swap inside the main function. So let's say that mute a is equal to one and that mute b is equal to two. We'll call the function swap on a and b. The value that is returned will be b and a. So let's reassign this to a and b. What this code is doing here is it's replacing the value for A and B. After calling this function swap, A will hold the value for B which is two and B will hold the value for A which is one. This function swap
can also work with other data types. So now let's turn this into a generic type. The first thing that we'll do is put our type placeholder. Let's say that we have a type called A and then replace all this U32 with the type A. Notice that inside the main function, we didn't have to make any changes. All we had to do was declare a type placeholder. So now we created a function that works with generic types. We'll make one more improvement. The type B doesn't have to be equal to type A. We can have two different types. So let's declare a new generic type. Let's call this type B. And then replace B with B. How about the return type? Well, the return type is going to be B and
then A. So now the swap function takes in two inputs A of the type capital A and B of the type capital B. It simply swaps the order and returns B and then A. The return type is capital B and then capital A. We now need to update our main function. Let's say that type A is of the type U32 and type B is I32. We pass in U32 and then I32 and then to A we're reassigning the type I32. and then to the variable B, we're reassigning the type U32. So this code will not compile.
Try to execute the code and you can see that the code does not compile since we're trying to reassign values of the different type. So let's update the code and fix the compilation error. The solution here is simple. All you have to do is remove the mute keyword and then reassign the variables. So A here will be of the type U32. B here will be of the type I32. But on the next line, we're redeclaring the variables. So now A will be of the type I32 and B will be of the type U32. Let's execute the code again. And this time the code compiles.
So in this video I showed you some examples of generic data types, how to declare it and how to write functions using generic types. Imagine that we have a strct called point which represent a point in 2D space. It will store two fields, the x coordinate and the y-coordinate as f32. In this video, I'll show you how to declare methods and static methods on a struck. The syntax that I'll show you works both for strrus and for enums. But in this video, we'll be using a struck. First of all, what is a method? A method is simply a function that is attached to the data type. In this example, we're using the data type point. We're going to be attaching a function to this strct point by declaring methods. What would this allow
us to do? Well, inside the main function, let's first create a point. Let's say that P is equal to point with X equal to 0.0 and Y equal to 0.0. By declaring a method, let's say that we declared a method to move the point. Let's say move to somewhere here. It doesn't matter what we put in. By declaring a method, the function will be attached to this instance of a point. So that we can use a syntax like p.ove to to call a function. Now, in contrast, if you didn't have a method, then we would need to have a function called move to and then pass in the point. In this case, inside the main function, the syntax will look like move to and then we pass in the point. So, that's the
difference. Now, how do we declare a method? We start with the keyword implement the methods for. This is for the point. And then inside here, let's create a method that operates on an instance of a point. So there's going to be two distinctions. There's going to be functions that operate on an instance of a point and there's going to be functions that operate on the type. The functions that operate on the type are called static methods, also known as associated functions. The functions that operate on an instance of the type are called methods. Let's first create a method. A function that operates on an instance of a point. Let's create a function. Let's say move to. This
function will update the x and y coordinates of the strruct point. So it's going to take a mutable reference to the point. Since it operates on the instance of this strruct point, we refer to this as self with a small s. And then let's put in the two new coordinates x of the type f32 and y of the type f32. We'll update the x coordinate by saying self.x is equal to the new x value. And then we'll do the same for the y-value. So this is an example of a method since it operates on the instance of the type.
How about a static method also called associated function. The static method will operate on the type. A good example is a static method that will instantiate the type. For our example, let's say we have a function called new. This function is going to create a new instance of the strct point. For the inputs, we'll pass in the xcoordinate f32. and then y-coordinate f32. And then we'll return this type point. Here we can declare this as a point or as self with a capital s. Self with a capital s refers to the type. In this case, this will be point. And then inside here again we can say point and then pass in the x and the y inputs. Or we can also
say self. They both do the same thing. Okay. So this is an example of a static method. Let's now call these two methods inside the main function. Here we have a point. So we can call the method move to to update the x and the y-coordinate. To do that, we first need to redeclare this variable as a mutable variable. Then we'll call p.ove 2. Then let's pass in for x, let's say 1.0. For y, let's say 2.0. Let's try to print this point out. To do that, make sure that the derived debug attribute is attached to the struck point. And now let's print this out. execute the code and we get the point printed out.
Xcoordinate 1.0, Ycoordinate 2.0. Now let's look at an example of a static method. This function will create the new point with the X coordinate and the Y-coordinate. We can actually modify this example. Instead of directly creating a point, we can call point colon the function U that we defined and then for the X coordinate that's passed in 0.0 and for the Y coordinate, we'll also pass in 0.0. the code over here and the code above does the same thing. So this is an example of a static method and this is an example of a method. To declare methods or static methods, you prefix the type with the keyword implections.
Imagine a situation where we have a function and we want to pass in various types for the input. For example, let's say that we have a strct called solidity and viper, two programming languages and then we have a function called compile. What we want from this function is that it would take in a language and then output a command that will compile the programming language. We want this function to be able to take in both Solidity and Viper. In Rust, we can accomplish this by declaring that the input is some type that implements a trait. Instead of declaring the input as a concrete type, we'll declare it as a trait. This tells Russ that the input that is passed in is some type that implements a trait that we define.
There's two parts to a trait. Defining a trait and then implementing a trait for a type. In this example, we want the types that we're going to pass in here as language to have a single function called compile, which will output the command to compile the language. To define a trait, we start with the keyword trait. Let's name this trait compiler. All types that implement the compiler trait will have a command to compile the language. Let's call this function compile. It will take in two inputs. the self. This will be the type that implements this compiler trait. And let's also pass in another input. Let's say that the file path to compile file path of the type string slice. Here we're passing in a string slice since
the file path can be read only. And then it's going to output a string that represents the command to compile the contract at this file path. So now that we defined a trait, let's now implement the trait for the strct solidity and biper. To implement the trait, we'll start with the keyword implement imple compiler and then followed by the type that's going to implement this compiler trait. For let's start with solidity. We need to implement the compile function. For simplicity, we won't worry about whether the command is a correct command or not. This is just a simple example. Let's say format. To compile a solidity contract, we'll need to call a command called sulk solidity
compiler. And let's say we also want to pass in the file path. By the way, this format macro is used for string interpolation. Here it's saying inside these curly braces, put in the input which will be file path. This will produce a string with sulk followed by file path. Now that we implemented the function compile for solidity, let's also do the same for viper. I'll copy this code and then paste it here. Compiler for Viper. Let's say that the command to compile biper contract is biper. Now, I'm not sure if this is the right command, but for this example, it will do. We're not so worried about the actual command to compile a contract.
We're focusing on how to declare traits and how to implement traits. Okay, so now we have the strct solidity and biper that implement the compiler trait. Now, let's move on to the function compile. Both the solidity and the biper strct implement the compiler trait. So now we can specify here for the input say that the input lang will be a type that implements the compiler trait. To do this we'll type imple and then followed by compiler. Now remember in rust that all data sizes must be known at compile time. Here it's saying some type that implements the compiler trait. This type currently is a unknown size. To make it a known size, we put it behind a
reference. Now this code will compile. Since whatever type that implements the compiler trait is behind a reference and the size of a reference is known at compile time. Inside the function body, we'll call the function lang. compile. The function compile takes two inputs. Self will refer to the instance of the type that implements the compiler trait. In this code, this will be a instance of biper. The other input is the file path. Let's pass the file path from the input to the function compile. say file path of the type string slice and then pass it into here file path. Now let's see some examples of how to call this function compile and also how to
directly call the function compile on the trait that implements the compiler trait. The first step is to initialize these two strcts solidity and biper. So let's start with solidity. Let's say that soul is equal to solidity. It takes in a single input to initialize this strct version. This will be a string. Let's say 0.8 to string. Next, we'll create the biper struck. Let's say let by equals biper and then version. Let's say 0.4 dot to string. The first way to call the function compile is to simply call soul.compile or viper.compile.
Let's print this out. For the file path, let's say hello. and hello. Vy execute the code and it says solidity compile. So hello. So biper biper hello.vy. So that's the first way to call the function compile on solidity and biper. The other way to call the function compile is to simply pass it into the function compile. So we can say let's copy these and then paste it here. So we'll call the function compile passing in a reference to the solidity strct. Here it is reference to something that implements the compiler trait and the solidity strct implements the compiler trait. And for biper we'll do the same something that implements
the compiler trait. This will be a reference to biper. Let's execute the code again. And again we get the same outputs printed. So these are two ways to call the function compile on a type that implements the compiler trait. We can directly call it or we can pass it into a function where the function takes in a trait as input. So now notice what we have done. The function compile takes any input that implements the compiler trait. In our example, we are able to both pass the solidity and the biper struck into this function. For the final part of this video, I'll show you that we can have a default implementation for the trait definition. Inside the compiler trait, we define a function called compile. All traits that implement the compile trait must
implement the function called compile. Let's also create another function. Let's call this help. This function when it is called will return some documentation on how to use the compiler. Inside the trait definition, we can also define a function. In this case, this will be the default implementation. For example, let's say that when the function help is called and the function help is not defined inside the implementation. In our example, we'll leave a blank implementation for the function help on solidity and piper. In this case, the code inside here will be executed. So let's say that when the function help is called, we'll return a unhelpful documentation. Let's say good luck. Now
notice that both for solidity and biper, we don't implement this function called help. This will be the default implementation. And now going to the main function, we'll be able to call the function help. Let's print these out. Let's copy these and then modify it. call the function help on the solidity strct and then call the function help on the viper strct. Execute the code and we get soul help good luck viper help good luck. So in this video we looked at example of how to define and implement a trait so that we can pass multiple types as a function input. By now I hope that you know what generic types and traits are. We can combine those two together to have a generic trait. Let's start
with a trait without generic types. Let's say that we have a trait called list. This will have two functions fn count which will take in a reference to self and then return a use size. Any type that implements the list trait will be able to return the number of elements that is contained inside that type. Let's also say that there's a function that will get the first element in the list. Let's say first it's going to take in self and then it will return a reference to some type. For now, let's make this a concrete type. The first element will be a reference to U32. So here we're assuming that a list is a collection of the type U32 having the function called count. And when we call
the function first, it will return the first element which will be of the type U32. Now let's make this into a generic trait. To do this, we'll put a T over here, the placeholder type. And then we'll replace this U32, the concrete type, with our placeholder type. And this is how you define a generic trait. Let's implement the list trait for a tpple of the type U32 boolean and character. So we'll type imple list. What is the concrete type that we need to put in here? Let's come back to this later. So we're going to implement the list trait for the tpple having the type U32 boolean and character. Let's start with the count function. This is a easy one. How many elements are inside a tpple of size three? Well, it is three.
it must return the first element inside the tpple. So we can do this by saying self.0ero. Since we're returning a reference, we'll put a percent sign here. Okay. Now we can answer the question, what is the type that must go in here? The type that must go in here is of the type T. When we return the zero element in the tpple U32, this T is of the type U32. So the type over here that must go in is also U32. So this is an example of a tpple having a size three that implements our list trait.
Let's look at another example. Let's implement a trait for a generic vector. So say imple list and then the type we don't know yet. For a vector let's say a generic vector. So this will be of the type t. Again we'll need to implement two functions counting first. So I'll copy these and then paste it here. What is the size of the vector? We can get the size of the vector by calling self.length the length of the vector. How do we get the first element of a vector? Assuming that the element at zero index exists, we'll directly access it. Self of zero.
What should be the type over here? It can be U32, but it can also be something else. Since here we have a generic vector. If this vector was of the type U32, then these types are aligned, so they are correct. However, here we're using a generic vector type T. So when we access the zero element in this vector, it's going to return a reference to type T. So now we can answer what type should go in here. This should be T. This type T that you see over here is always the same as a type that we return from the function first. Since here we're using a generic type T, we need to declare that this is a generic type. To do this, we put a T over here. The code here looks a little bit confusing. All
it's saying here is that the generic vector of type T implements the list trait and this type T that we declare over here is a generic type. Now let's call these functions inside the main function. So let's start with the tpple. Let's say that T is equal to let's put 1 U32. Next is boolean. So let's put a true. And for character I'll put the character C. And then let's call the function count and first t.ount count and t dot first. I forgot to put a semicolon here inside the definition of trait. Let's try to execute the code again and we get t dot count is three.
The first element is a one. Let's look at example of a vector. Initialize a vector with five elements and then call the function count on the vector and call the function first on the vector as well. Execute the code and we see that the count of the vector is five and the first element is a one. So in this video we looked at examples of how to declare and implement generic traits. Let's talk about trait bound. Traitbound is a way to tell the Rust compiler that a type implements a trait. This becomes necessary when you work with generic functions. So for example, let's say that we have a function called max. It will take in two U32 as inputs and then return the maximum of the two values X and Y. This function should also work with other number types such as I32 and
floats F32. So we want to make this function into a generic type. Let's do that by creating a type placeholder. Let's say T. It would take in a type of T, two types of T, X and Y, and then compare it and then return the same type T, the larger of X and Y. But now we have a problem here. There's no guarantee that this type T can be compared. For example, for the type T, if you pass in a vector, then it doesn't make sense to compare two vectors. So what we need to tell the rest compiler is that this type will implement a trait so that these types can be compared. The standard library in rust provides a trait called partial order so that we can compare the types. Now this function max will work with any data type that
implements the partial order. In other words, any types that can be compared. Now we can use this function to get the maximum of the values. And of course for the type key we can now pass in U32, I32, F32 and also characters. They all implement the partial order trait. This is a simple example of a trait bound. It specifies that the type we pass in will implement some other trait. Now let's look at more complex examples of trait bounds. The following examples will be a little bit abstract so that we can focus on the syntax and the capabilities of trait bounds. So let's say that we have traits A, B, and C. We can't put in functions inside these traits A, B, and C. But for simplicity, we'll leave it
empty for the function definitions of each trait. Let's say that U32 implements the trait A and B and I32 implements the trait C. Let's begin by a simple example. Let's say that we have a function called A and this function A will take in a single input. Let's say input X of the type T and the function does nothing. Now this type T must implement the trait A. To do this, T colon must implement A. This is the same syntax as this example over here. T implements partial order. X and Y are of the type T. Over here it's saying the input is of the type T and this T must implement the trait A. Let's actually
call this inside the main function. In our example, trait A is implemented by U32. So let's create a U32. Let's say that's U of the type U32 equal to 1. And we'll be able to call the function A passing in U. Since the function A takes in an input that implements the trait A. In this example, we won't be able to pass a I32 into the function A. This is because I32 does not implement trait A. And to see this, let's create a variable. let's say I of the type I32 and then try to call the function A passing in an I32. Execute the code and the code does not compile. Since the
type that we pass in here must implement trait 8. However, I32 does not implement trait 8. In this example, we only specified one trait bound. We said that type T implements trait 8. But we can also specify multiple traits for a type. So for example, let's say F. We'll call this function A B. The type T must implement A and B. It will take in a single input of the type T. So this function over here says that it takes in a single input of type T and the type T must implement trait A and B. Inside the main function, we'll be able to call the function A with the input U. U is of the type U32. U32 implements both A and B.
However, for the function A B, we won't be able to pass an I32 since I32 doesn't implement A or B. How about if you have another type that only implements the trait for B. Let's say F32. And then let's create a variable here. If you have a type F32, a float, and then try to call the function AB, this code will not compile because F32 only implements the trait B. However, the input to the function AB must implement both A and B. To see this, let's try to execute the code and as expected, the code does not compile. For the last example, let's look at a wear clause. It's a syntax so that this type declaration can look
nicer. Let's say that we have a function called W. The function W will take two inputs. Let's say X of the type T and Y of the type U. For the type T, let's say that it must implement A and B. And for the type U, let's say that this must implement B and C. This code might look a little bit messy. So here's another way to make this code look a little bit nicer. Let's copy this, paste it, comment this out. Here we'll say the generic types are T and U. We need to still specify that T must implement A and B and U must implement B and C. So this is where we can use the where syntax. We type where and then say t
colon a + b. t must implement a and b and u must implement b and c. This is an example of the wear syntax. The function that is commented it out and this function says exactly the same thing. The inputs are of the type t and u. T must implement a and b and u must implement b and c. So these are some examples of defining trait bounds. At first glance, they look a little bit confusing, but once you know how to read it, it's not so complicated. Every reference in Rust has a lifetime.
Lifetime tells the Rust compiler how long the reference is valid for. It's used to ensure memory safety and that no reference become invalid when the program runs. To see what can go wrong without lifetime, let's look at this example. Let's say that we have a function called longest stir which will take two string size as input. it will compare the length from these two string slices and then return the reference to the longer string slice. The problem with this function is that the reference that it returns may become invalid. To see how this is possible, consider that we have a string inside the main function. We create a new scope and inside this new scope we create another string. Call the function longest stir passing in the reference to X and Y.
assign the output of the function longest stir to a variable called Z and then try to print out the longest string from the reference Z. This code will not compile and furthermore the reference to Z may be invalid. In this example, comparing the length of the strings hello and rust, we know that hello is the longer string. However, if the variable Y is the longer string, what's going to happen here is that the function longest stir will return a reference to Y since Y is the longer string between X and Y. reference to Y will be assigned to the variable Z.
However, at the end of the scope, Y is dropped. This variable Z when you try to print it out will point to a reference that is invalid since Y no longer exists. So, this is example of reference that is being returned here becomes invalid. To prevent problem like this at runtime, we need to tell the Rust compiler that the reference that we're returning here will be valid as long as both the reference to X and Y are valid. To do this, we'll first need to declare a lifetime. We do this by putting a tick followed by the name of the lifetime. In this example, we'll name this lifetime A. The reference that is being returned here, let's call this has a lifetime of A. Now, both X and Y must be a valid reference as long as this reference that
is being returned is used. So, we need to specify that the lifetime of X is A and the lifetime of Y is also A. So this says that the reference that is being returned here will be valid as long as both x and y are valid. Now this code below inside the main function will still not compile since over here we specify here that the reference that is returned will be valid as long as both x and y are valid. How about the case inside the main function? Here we can see that the reference to y becomes invalid at the end of this scope. So the reference that is stored in z will outlive y. There are several ways to fix this code, but the simplest way is to move the variable Y to here and then
call the function longister. Now the reference to X and Y will be dropped at the end of the main function and the variable Z will hold a reference either to X or Y and the reference will be valid until the end of the main function. So now let's look at some few more examples of lifetimes. Similar to generic types, you can also declare multiple lifetimes. Here inside the function called print refs, it's declaring two lifetimes A and B. The input X will have a lifetime of A and input Y will have a lifetime of B. In this code example, we're simply printing out X and Y. How about for strrus?
Here's a str which has a single field called title which will hold a string slice. A string slice is a reference. So we need to put a lifetime here. The way we put a lifetime is similar to the syntax for generic types. Inside here we'll name our lifetime a. Then next to the amperand sign we'll put this a. So this is an example of declaring a lifetime for a str. How about on a method? Let's say that the book has a method called edit which will allow us to update the title. Since the title is a string slice, it needs a lifetime annotation. To do this for methods, we'll need to put a a here. We need to annotate the reference with a lifetime.
And to tell the Russ that this is a lifetime after imple. And then after book as well we'll need to put in a. This tells Russ that the book has a reference that has a lifetime of eight. And the new title will also have a lifetime of eight. And finally there are two special lifetimes. The first one is called a static lifetime. When we hardcode a string into a Rust program. This will create a string slice where the lifetime is static. Meaning that the reference is static. it is valid until the end of the program. Then we also have the placeholder type which we can make by putting the underscore.
This tells Rust to infer the lifetime. Okay, so these are some examples of lifetimes in Rust. Every reference has a lifetime and it's used to ensure memory safety and to make sure that references don't become invalid when the program runs. A while back, if you remember, I mentioned that you can use the for loop syntax to loop through a vector. Using this syntax, you can only loop it once. So if you try to loop it again and then execute the code, the code will not compile. Why is it that using this syntax we cannot loop through a vector twice? The reason has to do with iterators. When we use a syntax to loop through a vector, it's implicitly calling a function called into it. And when we wanted to loop through twice, we
need to explicitly call a function called it. So what's the difference between into it and it? And also what are they? Into iter and iter both produces a iterator. Iterator is a trait that says that the data type that implements the iterator can be iterated. When a data type implements the iterator trait, you'll be able to loop through the elements inside that data type. For example, vectors, array, and hashmap all implement the iterator trait. Data types that implement the iterator trait can be used with a for loop to loop through each element in that data type. Here I've listed two ways to create iterator.
There's another one called it mute. The difference between these three are into iter outputs a iterator where each item is of the type t. You get the value back. So the ownership is transferred over to the iterator. On the other hand, function it produces a iterator that for each iteration will give you back the reference to the value being iterated. Similarly, there's a function called iter mute. This is similar to the function iter. it will give you for each iteration a mutable reference to the value being iterated. So now let's go back to this when we call the function into iter. This value that you see over here will be of the type T. Since this vector is of the type U32, this will be
U32. The function into iter transfers ownership into this for loop. So once we call this for loop, we cannot call into it again since it already transferred ownership to this for loop. The function it on the other hand will give you back reference to the value. It is using the borrowing rule. Hence after calling this for loop we can call it again. And finally we have the iter mute. This will give you a mutable reference. Again since this is a mutable reference we can call it again. Create a iterator where for each iteration will give a mutable reference. So now that we know the difference between the functions into it, it and it mute, let's look at some examples of what you can do with a
iterator. Here we used it inside a for loop syntax. But once you have a iterator, you can do more. The most common functions that you can call are map, filter, and collect. So let's look at an example. Let's start with a vector of the type U32. We create a iterator by calling val. This is a vector. Call the function it. This will turn the spectre into a iterator. For each iteration, you'll get a reference to the type. The type here is a U32. So now let's look at an example of a map. Map for the input will take in a function. Let's call this f. And for each iteration, it will give you a single input of the type reference to T. The type that is being iterated.
Here the type that is being iterated is a U32. And then we need to get back another type D. Here the type T is U32. The map function will allow us to take each element inside the thing that is being iterated and then transform it to something else. For example, we can take each element in this vector and then plus one. To call the map function, we call map. Inside here, we need to pass in a function, a closure. It's also called the anonymous function. To create a closure, we put two vertical bars and then optionally we can put curly braces. Inside the vertical bar, we put the inputs. Function is going to take a single input. Let's call this X. Here you don't need to put type annotations but to be clear I'll show you what the type is. The type is reference to U32.
This is because it gives you a reference to the type D that is being iterated and then inside the curly braces you put your code in and at the end of the code you'll need to return a U32. For example, let's say X + one. The curly braces is used when you need to write multi-line code. For example, maybe you need to print something out. Let's say X is equal to X. Maybe you need to print it multiple times. In this case, you'll need the curly braces. However, in our case, the code is short and simple. It's simply adding one to x. And in this case, we can omit the curly braces and make code into a single line. The map function will produce another iterator.
So, if you want to create a vector where each element is incremented by one, we need to tell Russ to collect the result into a vector. To collect our result of the iterator, we call the function collect. Now the collect can collect into different data types that implement the iterator tra. So we need to tell Russ that the data type that we want to collect the result into is a vector of the type U32. So this is an example of the function map and collect. Let's print this out. Print ln b2.
Run the code and we get the vector elements printed out. 2 3 4 5 and 6. Each element inside the original vector was incremented by one. Now here we collected our result into a vector of the type U32. But we can also use the function collect to collect the result into a different data type. For example, let's say that we have a vector of a tpple. Each tpple is of the type string slice and U32. We can collect our result into a vector of a tpple where each tpple is of the type string and U32. For each iteration, we turn the string slice into a string. For this example, we will also increment the value u32 by one and call the function collect to collect our result into this vector. Now we can take the same code and then collect it into a
hashmap. Now here's the surprising part. We don't have to change any of this code over here. All we have to do is change the data type to a hashmap. A hashmap of the type where the key is a string and the value is a U32. Since Russ knows how to convert a vector of a tpple where each tpple is a string and U32 into a hashmap where the key is a string and the value is a U32. Here all we have to do was change the declaration of the data type that we're collecting our result into. Let's print the two results that we have over here a vector and a hashmap and also import the hashmap. At the top of the file I'll type use std collections hashmap. Okay, let's run the code and we get the vector printed out
and then on the next line we have the hashmap printed out. So that is the usefulness of using iterators where it's the function collect. If you were to write this same code using for loops for each iteration we need to explicitly write how to store into a vector and how to store a value into a hashmap. For a vector we need to push a tpple. For a hashmap we need to insert the value into the key. However, by calling the function collect, we can ignore the details and let Rust handle this detail. And finally, for the last example, let's look at another common iterator adapter called filter. Iterator adapters are functions that we can call on a iterator. Some of the common ones are map, filter, and collect. We're now going to look at example of a filter,
and this time we're going to call filter and then chain it with a call to the iterator adapter map. We'll begin by initializing a vector. Let's start with this vector over here having the values 1 to 5. Paste it here. So we'll call balsiter. Turn it into an iterator and then let's call the iterator adapter filter. Similar to the map filter takes in a single input. An anonymous function to filter out each element in the iteration. What this is going to do is going to give you each element inside the thing that is being iterated. If you return true then this means to keep the element. If we return a false here then this means to disregard the element. So here we need to pass in a closure. Let's
call the input x. Now the type that is being passed here will be a reference to the item that is being iterated. When we call the function it each item that is being iterated is a reference to the original type. The original type is U32. So this will be U32. What we have here is a double reference to U32. To dreference it, we need to do X and then we need to dreference it again to get the actual value of the U32. Let's say that we wanted to filter all values that are less than or equal to 3. Keep all values that are less than or equal to 3.
Now let's call the function map. Map will take another anonymous function as input. And here we need to instruct how to transform each element inside the thing that is being iterated. The input x will be of the type iter will give a reference to the type for each iteration. This will be a reference to u32. For this example, let's increment each value by one. Here we're passing in a reference, but rust is smart enough to convert this reference into the actual value. And then finally call collect. We'll collect our result into a vector. Let's say that be of the type vector of u32. And finally, let's print our result. I'll call this filter and then map. Execute the code and we get filter and map returns a vector having the
values 2, three and four. So this is example of chaining filter and map. Now let's convert this into using into iter. So when we do into iter for each iteration, we're going to get the value back. We're taking ownership of the vector val. Filter will return a reference to the thing that is being iterated. The thing that is being iterated is the U32. So now we can remove this double dreferencering for each iteration. We're also getting the actual value back. So here this will be U32 and not reference to U32. Let's execute the code again. Make sure that the code compiles. Okay. And we get the same result back. So in summary, first we looked at the difference between the functions into it and it mute into iter
for each iteration will give the value. It will take ownership of the data type that is being iterated. Itter will give you a reference to the value. Intermute will give a mutable reference to the value being iterated. And then we looked at examples of iterator adapters. Map, filter, and collect. Let's make hamburgers concurrently as fast as possible using async and a weight. To make a hamburger, we're going to need tomato, lettuce, cheese, patty, and a bun. A hamburger is a collection of all of these ingredients. To make a hamburger, we'll need to follow several steps. For example, we'll need to prepare the vegetables, toast the bun, and cook the patty. We'll represent the steps that are needed to prepare the ingredients as functions. So, we'll need a function to toast a bun, cook a
hamburger patty, get the veggies, and get the cheese. We also declare a function that will make a hamburger. We first toast the bun, cook the patty, get the veggies, get the cheese. We also print that the hamburger is ready, and then put all of the ingredients together. This will return a hamburger strct, which represents our hamburger. Inside the main function, we can make a hamburger by calling the function make hamburger sequentially. What the code is doing, it first toasts the bun and then cooks a patty and then gets the veggie and then gets the cheese and then puts all of the ingredients together.
However, we can make hamburgers quicker by using concurrency with async and await syntax. So, we're going to be transforming all of these functions using async await syntax. And we'll make the hamburger faster by using concurrency. Let's start with the basic. Let's turn this function into a async function. To do this, all you have to do is prefix it with the keyword async. This will turn a function into a async function. We'll do the same for all of the functions. Async over here, async for get veggies and async for get cheese as well. We also do the same for make hamburgers. Async. Since now the function toast bun, cook patty, get veggies and get cheese are all async. To
get the values out of a async function, we need to call the function a weight. A function that is declared async says that the value that is returned is not immediately available but it will return this value in the future. The await keyword says to wait here until a result is returned. In this case, the code is waiting here until a button is returned. And then we do the same for cooking a wait over here. The same for get veggies and get cheese. So now we've successfully turned the regular function into async function. Now to be able to execute async code, we'll need to install a library. So inside cargo.tl, we'll install the library that will enable us to execute async code. There
are several libraries that will allow us to execute async code, but probably the most popular one is called Tokyo. So in this video I'll be using Tokyo to execute async code. Now let's go back to our example asyncrs. So there's several things that we need to do so that we'll be able to execute async code inside the main function. The first thing is to declare async runtime. Asynchronous code are scheduled and executed by a runtime. So if you have async function calls, we'll need a async runtime to be able to execute those code. Since we're using Tokyo in Tokyo, we declare the Tokyo runtime as Tokyo col main. This will declare the async runtime. Now remember that to wait for a result from a async
function, we had to call await. The function make hamburger seek is also async. So to wait for the result, we need to call a weight. And finally, since we're using a weight inside here, we'll need to declare this main function also as async. Okay, so that does it. Let's execute the code. run the code and we get that the hamburger is ready. So this is example of turning a regular function into a async function. Let's now make the process of making the hamburger faster. Currently we're making a hamburger by first toasting a bun and after the bun is ready we cook a patty and then after the patty is done we get the veggies, get the cheese and then put all of the ingredients together. But if
you're cooking in real life you can do some of these concurrently. For example, you can toast bun while you're cooking the patty. And while the patty is still cooking, you can prepare the veggies and the cheese. We can express this and also execute some of these functions concurrently using the async aait syntax. Let's modify this function so that some of the internal function calls are executed concurrently. I'll copy this code and then paste it here. We'll rename the function as make hamburger. Now to execute some of these async functions concurrently, we'll use a macro from Tokyo called Tokyo colon join and then inside here we'll paste our function calls. This is a macro that will execute all of these functions
concurrently. It will return the result in a tuple. The first result is a bun. The next result is a patty. The next are the vegetables. The vegetables are tomato and lettuce. And then the final result is cheese. So using the drawing macro all of these functions will be executed concurrently. This is saying while toasting the bun also cook the patty also prepare the vegetables and also prepare the cheese. Once all of this is run give me all of the ingredients and then we put it inside the hamburger struck. Let's execute this code inside the main function. The function name is called make hamburger.
Execute the code again and we see that the hamburger is ready. Now when you call async function the aait keyword is important. If you don't call this then your code will not execute. When you call async function without the await keyword what it's actually returning is called a future. A future represents a value that will be returned in the future. So let's say that fuse is equal to make hamburger. This will return a future and a future is not executed unless you a wait on it. So to show you this, let's execute the code again. Now notice that the log hamburger is ready is absent. The code was not executed.
The hamburger was not cooked. To execute this future, we need to call await or we can get a future first and then call await on it. Now execute the code again. Now we get our hamburger back. So that is the basic of writing async away code in Rust. Now I want to compare using threads versus async await. Here we made our hamburger using async aait syntax, but we could have also made our hamburger concurrently using threads. So what's the difference between using threads and the syntax async a weight?
And when should you use one over the other? We could have written our function make hamburger using threads, but why didn't we? One of the problem of spawning threads is that spawning too many threads can crash this program. And to show you this, I'll show you a code example where we spawn multiple threads and then make hamburgers inside each thread. What you're going to see is that the program is going to crash. Here's a code that will try to make 1 million hamburgers concurrently. You don't have to worry too much about what's going on here. Since smart contracts written in Rust will probably not be able to spawn threads. To give you a quick explanation of what's going on here, we loop a million times. For each iteration, we spawn a new thread and inside each thread, we simulate a making of a
hamburger. We wait 100 milliseconds and then print that the hamburger is ready. When threads are spawned, they return what's called a handler. This handler must be waited inside the main function. So the main function will exit only after all of the threads has completed their execution. That is what this part of the code is doing. It's waiting for the threads to return their result. So that is basically it. Let's try making 1 million hamburgers concurrently using native threads. Execute the code and we see that after a while the program crashes. There are various reasons why the program crashed. Some of the problems with spawning too many threads is that your computer may have a limit on the number of threads that it can spawn or maybe spawning too many threads
exhausted all of the memories. So these are some of the issues that you might encounter when you spawn too many threads. However, you won't have this problem when you use async a rate code. To show you this, I'm going to write this code using async a rate syntax. So, I'll copy this, paste it here, and then comment out the code that spawns threads. We're going to try to make 1 million hamburgers concurrently. To do so, I'll first copy the code inside here inside thread.spawn and then paste it here. We'll modify this function into async future. Say async. This will be a future. Let's say that cute is equal to and then we'll replace this std thread sleep with Tokyo sleep colon time sleep for a duration of 100 milliseconds.
Since this is now a future we need to wait a wait on this. So it's going to sleep for 100 milliseconds and afterwards we'll print the hamburger that is ready. So this is our future. Now to do something similar to spawning a thread, we'll call Tokyo task spawn. Tokyo colon task colon spawn and then passing in the future. When we put a future inside a spawn, that future will be executed elsewhere so that we don't have to wait for this future to return a value. Now, similar to the thread, calling task col spawn will also return a handler. So let's say that handler equals and then we'll push this handler into our vector handles. handles dotpush handler. For each iteration, we're
creating a new task without waiting for the task to complete. To wait for all of these tasks complete, we'll need to modify this part of the code. This code was for the native bits. We need to modify it and then wait for the tasks that we spawned over here. Since the handlers that are returned over here are futures here. Instead of calling the function join, we need to call dot a weight. Okay. And that completes our code. I made a mistake. Before we execute the code, let me fix this mistake. The mistake is over here. This is not a future. This is a closure, an anonymous function. Since we have a vertical bar here, I'll remove this so that it becomes a future. Now, let's execute the code. Our code executed and we were able to make 1 million
hamburgers concurrently. To see that hamburgers were made concurrently, you can see that the numbers that are listed here do not come in a sequential order. For example, here we have 999,756 followed by 313,878. So in summary, we saw a limitation with native threads. Spawning too many native threads can crash the program. However, by rewriting the same code using async await syntax, we were able to make the program run without crashing. So when should you use native threads versus async a weight? A general guideline is when you need parallel computation, you should use native threads. On the other hand, if your function calls have a lot of waiting time, then you should use async a weight. In our example, making
hamburgers are not heavy on computation. There's a lot of waiting time for the ingredients to become ready. So in this case using the async a rate syntax is better than using threads. The Tokyo library has two useful macros called join and select. I've already introduced the join macro. So in this video we'll review what the join macro does and then I'll explain what the select macro does. The join macro executes multiple futures concurrently. It waits for all of the futures to complete and returns a couple of their results. The select macro also executes multiple futures concurrently.
In contrast to the join macro, it returns as soon as one of them completes and cancels the rest of the feature that is still executing. Basically, join says wait for all of the results to return and select says just give me one of the result that returns the earliest. Let's look at some examples. For the examples that I will be showing you, I've created a utility function that will wait for some time dt specified by the input and then it will return the string lit that was passed as input. Now let's look at example of a join macro. To use the join macro, we say join. And then inside here, we'll need to pass in our future.
So for example, let's say make coffee and coffee, it let's say takes 100 milliseconds. Calling the function make will return a future. Inside the join macro, we pass in futures as input. Let's also make green tea. Let's say that making green tea takes about 50 milliseconds. And finally, we'll make a lemonade. Let's say that making lemonade takes 20 milliseconds. And then let's assign this to a result. Let the first result rest one, the second result rest two, and the third result rest three.
The joint macro will return the three results after all of these are done executing. The feature that takes the longest to execute is coffee, which takes 100 milliseconds. We can expect that the result will be returned after 100 milliseconds has elapsed. Let's print these results out and then execute the code. And then we get our results. Coffee, green tea, and lemonade. So the join macro returns the results when all of the futures are done executing. Now let's take a look at select. The select macro will return a future that completes the earliest. To use the select macro, we start with select and then followed by curly braces. Inside here, we'll put all of the futures that we want to execute. This is make coffee,
green tea, and lemonade. For each future that returns a value, we need to assign a variable. So let's say bow is equal to and then once we get this bow we need to specify what we want to do with it. For the first feature we'll log future one finished first and then return the value. You'll do something similar for the second future and the third future. So the select macro will execute all three futures concurrently. The earliest future that completes will further execute the code inside. If the first future completes first then this code will be executed. If it is the second then it will be this code and so on.
Let's assign the value that is being returned to a variable. Let's say let rest is equal to and then finally let's print the result out and then execute the code. The result for the select is lemonade. We can sort of see this since lemonade takes the shortest amount of time to make. But now if we change all of these futures so that the waiting time for these future are all the same. What you'll see is that sometimes it will return lemonade, other times it will return coffee and other times it will return green tea. So let's execute the code again several more times. Okay, the select macro this time return green tea. Execute again and then returns coffee. Execute again and this time it returns lemonade. So these are some examples of working with the select and
the join macro.