How does versioning of the Rust language work

The programming language Rust

Let's take the plunge into Rust by working through a practical project together! This chapter introduces you to some common Rust concepts by showing you how to use them in a real-world program. You will learn,, methods, associated functions, the use of external crates and more! These ideas are discussed in more detail in the following chapters. In this chapter you will practice the basics.

We are going to implement a classic programming problem for beginners: a guessing game. Here's how it works: The program generates a random integer between 1 and 100. Then it will ask the player to enter an estimate. After an estimate has been entered, the program indicates whether the estimate is too low or too high. If the guess is correct, the game issues a congratulatory message and exits.

Set up a new project

To set up a new project, go to the directory projectsthat you created in Chapter 1 and create a new project with Cargo as follows:

The first command takes the name of the project () as the first argument. The second command changes to the directory of the new project.

Look at the generated file Cargo.toml at:

File name: Cargo.toml

If the information about the author that Cargo received from your area is incorrect, correct this in the file and save it again.

As you saw in Chapter 1, generate a "Hello, world!" Program for you. Look at the file src / at:

File name: src /

Let's compile this "Hello, world!" Program and execute it in the same step with the command:

The command comes in handy when you need to iterate a project quickly, like we're going to do in this game, by quickly testing each iteration before moving on to the next.

Open the file src / again. You will be writing all of the code to this file.

Processing an estimate

The first part of the guessing game program asks for user input, processes this input and checks whether the input is in the expected form. At the beginning we allow the player to enter an estimate. Enter the code from code block 2-1 in src / a.

File name: src /

Code block 2-1: Code that receives and outputs an estimate from the user

There's a lot of information in this code, so let's go through it line by line. To get user input and then output the result as output, we need to bring the library (input / output) into scope. The library comes from the standard library (known as):

By default, Rust brings the Prelude just a few types within the scope of any program. If a type you want to use is not in the prelude, you have to explicitly bring that type into scope with a statement. Using the library gives you a number of useful functionalities, including the ability to receive user input.

As you saw in Chapter 1, the function is the entry point into the program:

The syntax declares a new function, the brackets indicate that there are no parameters, and the curly bracket begins the body of the function.

As you learned in Chapter 1, there is a macro that prints a string of characters on the screen:

This code emits a prompt stating what kind of game it is and prompts the user for input.

Saving values ​​with variables

Next, we'll create a place to store user input, like this:

Now the program gets interesting! There's a lot going on on this little line. Note that this is a statement that is used to set up a variable to create. Here is another example:

This line creates a new variable named and binds it to the value of the variable. In Rust, variables are immutable by default. We'll discuss this concept in detail in the “Variables and Variability” section in Chapter 3. The following example shows how to use in front of the variable name to make a variable mutable:

Note: The syntax begins with a comment and continues to the end of the line. Rust ignores anything in comments. These are discussed in more detail in Chapter 3.

Let's get back to the guessing game program. You now know that a mutable variable called introduces. On the other side of the equal sign () is the value that is bound to what the result of calling, a function that returns a new instance of a. is a type of string provided by the standard library that is a growable, UTF-8 encoded piece of text.

The syntax on the line indicates that a associated function (associated function) is of type. An associated function is implemented on a type, in this case, and not on a specific instance of one. Some languages ​​call this one static method.

This function creates a new, empty string. You will find a function on many types because it is a common name for a function that creates a new value of some kind.

In summary, the line created a mutable variable that is currently bound to a new, empty instance of one. Uff!

Remember that we have included the input / output functionality from the standard library in the first line of the program. Now we call the function from the module:

If we hadn't put the line at the beginning of the program, we could have written this function call as. The function returns an instance of, which is a type that represents a handle to the standard input resource for your terminal.

The next part of the code calls the standard input resource method to get input from the user. We also pass an argument to:.

The purpose of is to append whatever the user enters into standard input to a string (without overwriting its contents), so it takes this string as an argument. The string argument must be mutable so that the method can change the content of the string by adding user input.

This indicates that this argument is a reference that gives you the option of having several parts of your code access one piece of data without having to copy this data multiple times into memory. References are a complex functionality, and one of the main advantages of Rust is how safe and easy it is to use references. You don't need to know many of these details to complete this program. For now, all you need to know is that references, like variables, are immutable by default. Therefore, instead of writing, you have to write to make them mutable. (Refer to Chapter 4 for a more detailed explanation of references.)

Handle potential errors with the type

We're still working on that line of code. Although we are now discussing a third line of text, it is still part of a single line of logical code. The next part is this method:

When calling a method with the syntax, it is often advisable to include a line break and additional spaces to break up long lines. We could also have written this code like this:

However, a long line is difficult to read, so it is best to split it up. Now let's discuss what this line does.

As mentioned earlier, what the user types is written to the string we pass to them, but it also returns a value - in this case, type. Rust has a number of types named in its standard library: a generic one as well as specific versions for sub-modules, e.g..

The types are Enumerations (enumerations), often called enums are designated. An enumeration is a type that can have a fixed set of values, and those values ​​become the variants (variants) of the list. Chapter 6 deals with lists in more detail.

For are the variants and. The variant indicates that the operation was successful, and inside is the value that was generated successfully. The variant means that the operation failed and contains information about how or why the operation failed.

The purpose of these types is to encode information for error handling. As for values ​​of any type, methods are defined for values ​​of type. An instance of has a method that you can call. If this instance is a value, the program will crash and display the message you passed to as an argument. If the method returns a, it is likely the result of an error in the underlying operating system. If this instance is a value, it will use the value that is holding as the return value so that you can use it. In this case, this value is the number of bytes the user entered in standard input.

If you don't call it, the program will compile, but you will get a warning:

Rust warns that you did not use the value returned by, which indicates that the program failed to handle a possible error.

The correct way to suppress the warning is to actually write an error handler, but since all you want to do is crash this program when a problem occurs, you can use it. Chapter 9 shows you how to recover from mistakes.

Output of values ​​with placeholders

Aside from the closing curly bracket, there is only one more line to discuss in the code added so far, namely the following:

This line outputs the string in which we saved the user's input. The phrase curly brackets is a placeholder: Think of yourself like little crab pincers holding a value in place. You can use curly braces to output more than one value: the first set of curly braces contains the first value listed after the formatting string, the second set contains the second value, and so on. Outputting multiple values ​​in one call to would look like this:

This code would print.

Testing the first part

Let's test the first part of the guessing game. Do it with:

At this point the first part of the game is complete: we receive input from the keyboard and then output it.

Generate a PIN

Next we need to generate a PIN that the user will try to guess. The PIN should be different every time so that the game is fun more than once. Let's use a random number between 1 and 100 so the game doesn't get too difficult. Rust does not yet include any random number functionality in its standard library. However, the Rust team provides a box.

Use a box to get more functionality

Remember that a box is a collection of Rust source code files. The project we built is one binary box (binary crate) which is an executable file. The box is one Library box (library crate) containing code to be used in other programs.

Using external crates is where Cargo shines. Before we can write code that uses it, we need the file Cargo.toml modify it so that the box is included as a dependency. Now open that file and add the following line below under the heading of the section Cargo created for you. Make sure you are using Version, otherwise the code examples in this guide won't work.

File name: Cargo.toml

In the file Cargo.toml everything that follows a heading is part of a section that continues until another section begins. In the section you tell Cargo which external boxes your project depends on and which versions of these boxes you need. In this case we specify the box with the semantic version specifier. Cargo understands semantic versioning (sometimes also SemVer called), which is a standard for writing version numbers. The number is actually the abbreviation for what stands for all versions from and smaller than. Cargo assumes that the public API of these versions is compatible with version 0.8.3 and this information ensures that you receive the latest patch version, which can still be compiled with the code in this chapter. As of version, the API is not guaranteed to match the one used in the following examples.

Now, without changing the code, let's build the project as shown in Code Block 2-2.

Code Block 2-2: The output of running after adding the box rand as a dependency

You may see different version numbers (but thanks to SemVer they are all compatible with the code!), Different lines (depending on your operating system), and the lines may appear in a different order.

Now that we have an external dependency, Cargo pulls the latest versions of everything out of the Registrywhich is a copy of the data from is where the people in the Rust ecosystem make their open source Rust projects available for others to use.

After updating the registry, Cargo will check the section and download any boxes you don't already have. While we only listed as a dependency, in this case Cargo also grabbed other boxes that it depends on in order to function. After the boxes are downloaded, Rust compiles them and then compiles the project with the available dependencies.

If you run right back without making any changes, you will get nothing but the line. Cargo knows it has already downloaded and compiled the dependencies, and you have them in your file Cargo.toml nothing changed. Cargo also knows that you haven't changed anything in your code, so it won't be recompiled. Without having to do anything, it just ends.

If you have the file src / open up, make a trivial change, and then save and rebuild, you only see two lines of output:

These lines show that Cargo is only able to build with your tiny change to the file src / updated. Your dependencies haven't changed, so Cargo knows it can reuse what it has already downloaded and compiled. It just rebuilds your part of the code.

Ensure reproducible builds with the file Cargo.lock

Cargo has a mechanism in place to ensure that every time you or someone else builds your code, you can recreate the same artifact: Cargo will only use the versions of the dependencies you specify until you specify otherwise. For example, what happens if version 0.8.4 of the box comes out next week and contains an important bug fix, but also a regression that breaks your code?

The answer to this problem is the file Cargo.lockthat was created when you first ran and is now in your guessing_gameDirectory is located. When you first build a project, Cargo finds all versions of the dependencies that match the criteria and then writes them to the file Cargo.lock. When you build your project in the future, Cargo will see the file Cargo.lock exists and use the versions specified there instead of doing all the versioning work again. That way, you automatically get a reproducible build. In other words, your project stays thanks to the file Cargo.lock until you explicitly increase the version number.

Updating a box to get a new version

When you update a crate want, Cargo offers another command that will open the file Cargo.lock ignored and put all the latest versions that meet your specifications in Cargo.toml finds out. If that works, Cargo will put these versions in the file Cargo.lock write.

By default, however, Cargo only looks for versions greater than and less than. If the box has two new versions and released, you would see this when you run:

At this point you would also make a change in your file Cargo.lock notice that determines the version of the crate you are using now is.

If you wanted to use the version or any version in the series, you would have to use the Cargo.toml customize it to look like this:

The next time you run, Cargo will update the registry of available crates and reevaluate your email requirements according to the new version you specified.

There's a lot more to be said about Cargo and its ecosystem that we'll cover in Chapter 14, but for now, that's all you need to know. Cargo makes it very easy to reuse libraries, so the Rust developers are able to write smaller projects that are assembled from a number of packages.

Generate a random number

Now that you got the box too Cargo.toml added, let's start with. The next step is src / as shown in code block 2-3.

File name: src /

Code block 2-3: Adding code to generate a random number

First we add a line:. The trait defines methods that implement random number generators, and that trait must be in scope in order for us to use these methods. Chapter 10 covers features in detail.

Next we add two lines in the middle. The function gives us the special random number generator that we will use: One that is local to the current thread and is initialized (seeded) by the operating system. Then we call the random number generator method. This method is defined by the feature that we brought into scope with the statement. The method takes a range expression as an argument and generates a random number in this range. A range expression has the form. It includes the lower limit, but not the upper limit, so we have to specify to get a number between 1 and 100. Alternatively, we could give the range, which is equivalent.

Note: You will not always know which features to use and which methods and functions of a box to call. For instructions on how to use a box, see the documentation for each box. Another nice feature of Cargo is that you can run the command that provides the documentation provided by all your dependencies locally and opens it in your browser. If you are interested in other functions of the box, run, for example, and click on in the sidebar on the left.

The second line we added in the middle of the code outputs the secret number. This is helpful while we are developing the program so we can test it, but we will be removing it from the final version. It is not a real game if the program prints the answer as soon as it starts!

Try running the program a few times:

You should be given different random numbers and they should all be between 1 and 100. Great work!

Compare the estimate with the secret number

Now that we have user input and a random number, we can compare them. This step is shown in Code Block 2-4. Note that this code doesn't fully compile yet, as we'll explain.

File name: src /

Code block 2-4: Handling the possible return values ​​when comparing two numbers

The first new element here is another statement that brings a type named from the standard library into scope. Like is another list, but the variants for are, and. These are the three results that are possible when comparing two values.

Then we add five new lines below using the type. The method compares two values ​​and can be applied to anything that can be compared. It needs a reference to what you want to compare: This is where it is compared to. Then it returns a variant of the enumeration that we brought into scope with the statement. We use an expression to decide what to do next based on which variant of the call to with the values ​​in and returned.

An expression consists of Branches (arms). A branch consists of one template (pattern) and the code to be executed if the value at the beginning of the expression matches the pattern of this branch. Rust takes the value given at and looks through the pattern of each branch in turn. The -Construct and Patterns are powerful functionality in Rust that allows you to express a variety of situations your code might encounter and ensure that you handle them all. These functionalities are dealt with in detail in Chapter 6 and Chapter 18.

Let's go over an example of what would happen to the expression used here. For example, suppose the user guessed 50 and this time the randomly generated secret number is 38. If the code compares 50 to 38, the method returns because 50 is greater than 38. The expression gets the value and starts checking the pattern of each branch. He looks at the pattern on the first branch and sees that the value doesn't match, so he ignores the code on that branch and moves on to the next branch. The pattern of the next branch fits to! The associated code in this branch is executed and displayed on the screen. The expression ends because it doesn't need to look at the last branch in this scenario.

However, the code in code blocks 2-4 cannot be compiled yet. Let us try it:

The core message is that it is mismatched types (mismatched types) there. Rust has a strong, static type system. However, it also has type inference. As we wrote, Rust was able to conclude that a should be and didn't force us to specify the type. That, on the other hand, is a number type. Some types of numbers can have a value between 1 and 100:, a 32-bit number; , a 32-bit unsigned number; , a 64-bit number; as well as others. Rust uses what is the type of by default, unless you add type information elsewhere that would cause Rust to infer a different numeric type. The reason for the error is that Rust cannot compare a string and a number type.

Ultimately, we want to convert what the program reads as input into a real number type so that we can compare it numerically with the secret number. We can do that by adding another line to the function body:

File name: src /

The line is:

We create a variable named. But wait, doesn't the program already have a variable named? Yes, but Rust allows us to change the previous value from with a new value shadow (shadow). This functionality is often used in situations where you want to convert a value from one type to another. Shading allows us to reuse the variable name instead of forcing ourselves to create two unique variables, e.g. and. (Chapter 3 covers shading in more detail.)

We bind to the expression. That in the expression refers to the original one that was one with the input in it. The method of the instance will remove all leading and trailing spaces. Although it can only contain numeric characters, the user must press Enter to satisfy. When the user presses Enter, a newline character is added to the string. For example, if the user types 5 and presses Enter, it looks like this:. This stands for "newline", the result of pressing Enter. The method removes what just yields.

The method for strings breaks a string into a kind of number. Since this method can parse a wide variety of number types, we need to tell Rust the exact number type we want by using. The colon () after Rust says we're going to annotate the type of the variable. Rust has a couple of built-in number types; you see here is an unsigned 32-bit integer. It's a good standard choice for a small positive number. You can find out about other number types in Chapter 3. In addition, the annotation in this example program and the comparison with mean that Rust will deduce from this that there should also be a. So now the comparison is made between two values ​​of the same type!

Calling to could easily cause an error. For example, if the string contained it, there would be no way to convert it to a number. Since this could fail, the method returns a type, similar to the method (earlier in "Handling Potential Errors with the Type"). We will treat this in the same way, using again. If a variant of returns because it couldn't generate a number from the string, the call will crash the game and print the message we give it. If the string can successfully convert to a number, it returns the variant of, and returns the number we expect from the value.

Let's run the program now!

Nice! Even if spaces were entered before the estimate, the program still found that the user made an estimate of 76. Run the program a few times to check the different behavior for different input types: guess the number correctly, guess a number that is too large, and guess a number that is too small.

Most of the game works now, but the user can only a Make an estimate. Let's change that by adding a loop!

Allow multiple estimates using a loop

The keyword creates an infinite loop. We're adding these now to give users more chances of guessing the number:

File name: src /

As you can see, we've put everything into a loop from the estimate prompt. Make sure to indent each line within the loop by four more spaces and then run the program again. Notice that there is a new problem because the program does exactly what we told it to do: ask for another guess forever! It doesn't look like the user can exit the program!

The user could interrupt the program at any time with the keyboard shortcut Ctrl + c. But there is another way to escape this insatiable monster, as mentioned in the discussion in “Comparing the estimate to the secret number”: If the user enters an answer without a number, the program crashes. The user can take advantage of this to exit the program as shown here:

Entering actually ends the game, but this also applies to all other entries that are not numbers. However, this is suboptimal to say the least. We want the game to end automatically if the correct number is guessed.

Quit after a correct estimate

Let's program the game to end if the user wins by adding an instruction:

File name: src /

Adding the line after causes the program to exit the loop if the user guesses the PIN correctly. Leaving the loop also means exiting the program, since the loop is the last part of.

Handle invalid input

To further refine the game's behavior, we shouldn't crash the program if the user doesn't enter a valid number, but make the game ignore invalid numbers so the user can continue guessing. We can do that by changing the line that converts from to to, as shown in Code Block 2-5.

File name: src /

Code block 2-5: Ignoring an invalid number and asking for another guess instead of crashing the program

Switching from a call to an expression is the general transition from crashing on an error to handling the error. Remember that returns a type and is an enumeration that has the variants and. We use an expression here, as we did with the result of the method.

If is able to successfully convert the string to a number, it will return a value containing the resulting number. This value will match the pattern of the first branch and the expression will only return the value generated by and inserted into the value. This number will land exactly where we want it in the new variable we are creating.

If Not is able to convert the string to a number, it returns a value that contains more information about the error. The value does not match the pattern in the first branch, but it does match the pattern in the second branch. The underscore is a collection container; in this example we say that all values ​​should match, regardless of what information they contain. So the program will run the code on the second branch, which tells the program to go to the next iteration and ask for another guess. The program effectively ignores all errors that could occur with!

Now everything in the program should work as expected. Let us try it:

Fantastic! With a tiny finishing touch, we end the guessing game. Remember that the program is still issuing the PIN. That worked fine in testing, but it ruins the game. Let's delete what is issuing the secret number. Code blocks 2-6 shows the final code.

File name: src /

Code blocks 2-6: Complete code of the guessing game


At this point you have successfully set up the guessing game. Congratulations!

This project was a handy way to familiarize yourself with many new Rust concepts:,, methods, associated functions, using external boxes, and more. You will learn more about these concepts in detail in the next few chapters. Chapter 3 covers concepts common to most programming languages, such as variables, data types, and functions, and shows how to use them in Rust. Chapter 4 examines ownership, a functionality that sets Rust apart from other languages. In chapter 5 structures (structs) and the method syntax are discussed and in chapter 6 the functionality of enumerations is explained.