Ultimate Rust Crash Course | Nathan Stocks | Skillshare
Play Speed
  • 0.5x
  • 1x (Normal)
  • 1.25x
  • 1.5x
  • 2x
35 Lessons (2h 53m)
    • 1. Introduction

      3:29
    • 2. Exercises Overview

      0:55
    • 3. Cargo

      3:53
    • 4. Variables

      4:50
    • 5. Scope

      2:40
    • 6. Memory Safety

      1:33
    • 7. Exercise A - Variables

      4:55
    • 8. Functions

      2:06
    • 9. Exercise B - Functions

      3:23
    • 10. Module System

      3:50
    • 11. Scalar Types

      4:41
    • 12. Compound Types

      2:08
    • 13. Exercise C - Simple Types

      6:25
    • 14. Control Flow

      5:23
    • 15. Strings

      4:43
    • 16. Exercise D - Control Flow & Strings

      4:28
    • 17. Ownership

      6:03
    • 18. References & Borrowing

      4:42
    • 19. Exercise E - Ownership & References

      4:49
    • 20. Structs

      3:09
    • 21. Traits

      4:57
    • 22. Exercise F - Structs & Traits

      2:47
    • 23. Collections

      2:54
    • 24. Enums

      7:18
    • 25. Exercise G - Collections & Enums

      6:29
    • 26. Closures

      2:25
    • 27. Threads

      1:52
    • 28. Exercise H - Closures & Threads

      7:57
    • 29. Invaders Part 1: Setup Audio

      4:25
    • 30. Invaders Part 2: Rendering & Multithreading

      18:26
    • 31. Invaders Part 3: The Player

      4:28
    • 32. Invaders Part 4: Shooting

      9:01
    • 33. Invaders Part 5: Invaders

      13:00
    • 34. Invaders Part 6: Winning & Losing

      8:13
    • 35. Final Words

      0:31
  • --
  • Beginner level
  • Intermediate level
  • Advanced level
  • All levels
  • Beg/Int level
  • Int/Adv level

Community Generated

The level is determined by a majority opinion of students who have reviewed this class. The teacher's recommendation is shown until at least 5 student responses are collected.

70

Students

1

Project

About This Class

Join Nathan Stocks for a fast-paced, entertaining, and curiously informative hands-on crash course in the Rust programming language.

Rust is a systems programming language that eliminates entire classes of bugs and security vulnerabilities, has zero-cost abstractions like C and C++, is fun to program in, and lets systems programmers have nice things. No wonder Rust is gaining traction in spaces as diverse as game engines, high-performance computing, embedded devices, and web programming! Learn how to write high-performance code without the worry of crashes or security vulnerabilities. Join a vibrant community of developers where diversity, inclusion, and just plain being nice are all first-class objectives.

Meet Your Teacher

Teacher Profile Image

Nathan Stocks

Rust, Python, and Indie Games

Teacher

Nathan Stocks has been a software developer for over 20 years. He fell in love with Rust in 2016 and began teaching it the following year. He experiments with Indie Game development in both Rust and more traditional game engines. He has used Python professionally for most of his career, and even wrote his own test runner called Green.

Nathan loves teaching Rust when he gets the chance, especially in person at conferences and corporate boot camps.

If Nathan had to pick his favorites, they would be: Rust, Python, PostgreSQL, Linux (server), macOS (desktop), vim and emacs, and whichever IDE has the best Rust support at the moment.

Nathan loves to spend time with his wife and kids, play frisbee, eat food, and play games. His ambition is to one day run his own so... See full profile

Class Ratings

Expectations Met?
  • Exceeded!
    0%
  • Yes
    0%
  • Somewhat
    0%
  • Not really
    0%
Reviews Archive

In October 2018, we updated our review system to improve the way we collect feedback. Below are the reviews written before that update.

Your creative journey starts here.

  • Unlimited access to every class
  • Supportive online creative community
  • Learn offline with Skillshare’s app

Why Join Skillshare?

Take award-winning Skillshare Original Classes

Each class has short lessons, hands-on projects

Your membership supports Skillshare teachers

Learn From Anywhere

Take classes on the go with the Skillshare app. Stream or download to watch on the plane, the subway, or wherever you learn best.

phone

Transcripts

1. Introduction: Welcome to the ultimate rust crash course. Let me introduce myself. My name is Nathan Stocks. I've spent the last 20 years or so working in software development, mostly on back end infrastructure. Though I have a day job, I moonlight as an independent game developer at agileperception.com and as an online course instructor teaching Rust, which is a fantastic systems language that I love. Since you are here, I bet you are interested in rust as well! Regardless of where you came from, Rust has something for you. You may not be sure if Rust is the right thing for you yet. You may intend to be like one of these moviegoers, and just want to watch and listen today and see if things sound interesting. That's OK; there is a time for that! I've been there...but if you really want to learn Rust today, I want you to actually DO something with what you learn. At the very least, do the exercises! Even better, make your own little project and experiment with things. You will learn by doing--and learning is important because Rust has a really steep learning curve at the beginning. Without learning the fundamentals, you won't even be able to get your code to compile--and fundamentals are exactly what we are learning today. This is a crash course in all the stuff you need to know to be able to start really using and learning Rust. I'm also going to teach you how to find answers on your own, so that after this course you will know where to go to learn more about the topics that you are interested in. Anyone can learn Rust. Anyone can be a systems programmer. Just have fun with what you know and learn a little more each day. Okay, so Rust is awesome! Let's talk about what Rust is, where it came from, and why it's so awesome. Rust is a systems programming language pursuing the trifecta: safety, which is guaranteed at compile time; fearless concurrency, which is a lot easier when things are safe; and blazingly fast speed due to zero-cost abstractions and other nice things. High level scripting languages like Ruby or Python will get you safety but not concurrency or speed. On the other hand, systems languages like C and C++ will give you speed and some access to concurrency, but forget about safety, because they will happily let you shoot yourself in the foot by design. So where did rust come from? Rust was started in 2006 as a personal project of a Mozilla employee named Graydon Hoare. Mozilla started sponsoring Rust officially in 2009 and version 1.0 was released in 2015, which makes Rust about five years old after a nine year incubation period. In comparison C++ is about 35 years old, and C is about forty five years old. Now, why would Mozilla sponsor a systems programming language? Because they were sick of C++ and wanted a better language to program Firefox in. Did you hear about Firefox quantum in 2017? It was a big update! Suddenly Firefox was over twice as fast, and less buggy! Why? Because the core of Firefox was rewritten in Rust. Today there is about 1.5 million lines of Rust in Firefox. So that's where rust came from. In the next section we'll talk about cargo one of Rust's most important tools. 2. Exercises Overview: Let's talk about how to get to the exercises for this course. This crash course comes with a companion repository on GitHub. It's under the user name CleanCut. The repository name is ultimate_rust_crash_course, separated by underscores. This repository contains helpful information about how to learn Rust as well as a set of exercises that you can run through. The exercises are contained in the exercise directory. You should clone or download this repository to your own machine. Once you have cloned the repository to your machine, each of the exercises inside of the exercise subdirectory is a standalone Rust project, the one exception being the first exercise, which is only a readme. For each of these projects you should open the project directory in your IDE or editor and open up the src/main.rs file and follow the instructions. 3. Cargo: Cargo is fantastic. It's the first tool that you will use and you'll keep using it for all sorts of things. Cargo is a package manager. Wait, a package manager for a viable systems programming language? Yes. Finally let that sink in. Systems programmers can have nice things too! You use cargo to search for, install, and manage packages that you want to use. Cargo is also the build system. No more make files! Cargo is also the test runner, and the documentation generator, etc.. It's like all of the good parts of NPM and Pip and bundler and make. Let's do something real. If you've installed Rust you can open up your terminal right now and follow along. If you type "cargo new hello" and hit enter, cargo will happily and colorfully create a project named hello for you. Let's take a look of a cargo created for us: a hello directory with a config file named cargo.toml and a source subdirectory with a main.rs file. Config files in Rust use the .toml format which stands for Tom's obvious minimal language. Rust source files use the .rs extension as you see on main.rs here. Now, launch your favorite editor that you set up and prepared with Rust support and open up Cargo.toml. Cargo.toml is the config file for your project. In fact, it is the authoritative source of information about your project. Name is the actual name of your project. It doesn't matter what the name of the directory is or what the name of your Git repository is. This setting determines the name of your project. Rust uses semantic versioning, which means a version number is always three numbers separated by dots and each number has a specific meaning. If you aren't familiar with semantic versioning already, please semver.org for more details because I'm not going to go into it any further. Cargo will search a bunch of likely places for your name and email address. If it's able to find it it will fill it out for you. One of the places that searches is your global Git config. Edition should be 2018. If cargo didn't add this line for you automatically then you are using an older version of Rust and you need to update it now by running "rustup update" Unless of course it's a higher year in which case you're far in the future and you can ignore that and maybe you should look for an updated version of this course. Later I will show you how to manage your dependencies here, but that's all we need to know about Cargo.toml for the moment. Now let's open up main.rs in the source directory. It already has a "Hello, world!" program for us. That's fantastic! What a helpful language! Let's go back to our terminal. "cargo run" is the command to build and run your project in one step. If you run your project now you'll see "Hello, World!" right after cargo finishes building. If you run it again you'll see that cargo can tell that nothing changed in the source code so it doesn't recompile the project. Notice what cargo is running. target/debug/hello target/debug/hello Let's take a look. There's a target directory where cargo outputs all of its build artifacts. This is definitely a directory you want your version control software to ignore. We can run the binary directly if we want. Did you notice that it's in a debug sub directory? Cargo compiles your project with debug symbols by default. Add "--release" to your "cargo run" command to compile without debug symbols. It's a lot faster this way. You'll notice a similar result, only this time it will be stored in the released subdirectory. Most code will run significantly faster in release mode, but it takes a little longer to compile. I suggest you keep this "hello" project around and use it for little experiments. In the next video, we'll talk about variables. 4. Variables: Let's talk about variables. They're pretty important. They look like this. First notice that Rust is a braced language with statements terminated by semicolons. Second rust intentionally mimics as much syntax from other languages as it can, C and Python in particular. So it should feel familiar right from the start for a lot of you. To declare a variable you use a "let" statement. Here we declare "bunnies" and initialize it to the integer value of 2. Rust is a strongly typed language, so where is the type annotation? Well, whenever Rust can figure out the appropriate type for you, you can just leave the type annotation out. You don't have to write "auto" or anything. When you do annotate the type it looks like this: a colon after the variable name, and then the type. "i32" is a signed 32-bit integer. We'll talk about primitive types in a later video. So that's how you initialize a single variable. It just so happens that the "let" statement has a trick up its sleeve that will allow us to initialize multiple variables at once. A "let" statement can destructure the data on the right hand side and use it to initialize variables inside of a corresponding pattern on the left hand side. We'll talk a little bit more about patterns in the later video as well. For now we'll settle for just using a tuple pattern when when we want to initialize multiple variables at once. Variables are immutable by default in Rust, which means unless you choose to make them mutable you can't ever change their value. That's unusual! Most languages default to variables that you can change. Why would Rust have immutable variables by default? Safety concurrency and speed! There are lots of bugs that can't happen if a value never changes. So safety is improved by immutability. Data that never changes can be shared between multiple threads without locks, so concurrency is improved by immutability. The compiler can also do extra optimizations on data it knows won't change, so speed is improved by immutability. That's why data is immutable by default in Rust: safety concurrency and speed. But let's face it: sometimes we need to change the value of a variable! We can't just change the value of "bunnies" as long as it is immutable. That would give us an error, this error in fact! Here's our very first example of our friendly compiler nudging us in the right direction! Let's break down this information. At the top we have a summary of the error: cannot assign twice to immutable variable bunnies. Next we have the location of the error: src/main.rs, line 3, column 5. Then, most amazingly, we have a contextual explanation of what we are doing wrong and how we might fix it. On line 2 we initialize an immutable "bunnies" to the value 16. Therefore on line 3 we cannot assign a different value. Immutable means you can't change it! What does the compiler suggest? Make the variable on line 2 mutable. If this information isn't enough to figure out the error, there is also a full explanation of this error type that you can get by running this command. You can go ahead and run this command right now if you like. "rustc" is the actual compiler that cargo uses under the hood to compile your code. Here's a version of the code that will work. It looks like this. "let mut bunnies = 32" OK so we have mutable variables that we can change and immutable variables that we cannot change. But there is also a kind of variable that is even immutabler: the constant. There are four things different about declaring a constant: First, "const" instead of "let". Second, the convention is to use screaming-snake-case for constants, meaning all uppercase words separated by underscores. Third, the type annotation is required. You can't just leave it out. And fourth, the value must be a constant expression that can be determined at compile time. The Rust team is adding more and more standard library functions to the constant list every release. So there's actually quite a bit of things you can do at compile time that counts as a constant expression. A literal always works just fine. So why would you go to all this trouble to use a const? Two reasons. First you can place a constant outside of a function at module scope and use it anywhere you want. A global. An immutable, constant global. Second, because const values are inlined at compile time, they are really fast! In the next video we'll talk about Scope. 5. Scope: Variables have a scope, which is the place in the code that you are allowed to use them. The scope of a variable begins where it is created and extends to the end of the block. Along the way, it is accessible from nested blocks. A block is a collection of statements inside curly braces. That includes function bodies. In this example "x" is defined in the main function's block. Then we create a nested block. Inside the nested block we create y, and then we print x and y, which works just fine because x is accessible from nested blocks. Then the block ends, and y is immediately dropped at this point. There is no garbage collector. Values are always immediately dropped when they go out of scope, which means the last print won't work. But fear not! We discover this at compile time! The error message is pretty clear. Cannot find value y in this scope. So, we would either need to hoist the second printline macro up into the same scope as y, or move y down to the same scope as the second print line macro. Variables can also be shadowed. Another way to think about shadowing is the variables are always local to their scope. Here we create a variable x and initialize it to 5 in the outer block. Then in the inner block x is shadowed with a new value 99. These two x's are different variables with different values. They just overlap in scope. The first print will see X as 99. Note that the value of the first x is not accessible from the inner block after it is shadowed. But as soon as the inner block ends, the inner x is dropped and the outer x is once again accessible. So the last print sees the value 5. You can also shadow variables in the same scope. Here we shadow a mutable variable x with an immutable variable x that we initialize to the first x's value. This essentially redefines the variable x with different mutability. Even cooler, the compiler will often optimize away this actual operation, and so nothing actually happens in the assembly code. You can even shadow a variable to a different type in the same scope, which some people like to do in data transformation pipelines that discard intermediate representations. So in this example, meme starts as the words "more cowbell" and then becomes an image. In the next video we'll go over Memory Safety. 6. Memory Safety: Rust guarantees memory safety at compile time. As a part of that, variables must be initialized before you can use them. This code won't work because enigma has been declared but not initialized to a value before we try to use it. In fact it won't even compile! We get the error" use of possibly on initialized variable: `enigma`". What if we might initialize enigma depending on some condition? The compiler won't reason about the value of a condition at compile time even if it's a literal true or false. Conditional evaluation is handled at runtime, so the compiler can't guarantee that enigma will be initialized before it is used because the compiler doesn't know what the value of true will be at runtime. So this still won't work. But this works! The compiler can tell that enigma is guaranteed to be initialized before it is used. As long as the compiler can guarantee something is safe, it will let you do it. What if you tried the same thing in C? What if you declared a variable and then used it before initializing it? Welcome to the glorious realm of undefined behavior! On my Mac I tried it and then I got the number 1. I'm not sure if that's just what happened to be in memory or what, but your mileage may vary because your compiler can choose to do anything it wants to do in this situation. Let's leave C and its craziness alone. In the next video we will go over functions. 7. Exercise A - Variables: It's time for exercise A. Exercise A can be found on GitHub in the repository CleanCut/ultimate_rust_crash_course in the exercise subdirectory and then in a-variables. For future exercises you'll need to clone this repository and open the exercise subdirectories in your IDE or editor, but for exercise A we only have a readme because the exercise is to create a project. So, open this exercise up in your browser and then take a look at it. I suggest you give it a try and see if you can do the entire exercise, including the challenge, by yourself! If you're able to do it just fine without any problems, feel free to skip to the next video but if you'd like to learn a little bit more, or see how I did it, then come back and I'll walk through the problem. So pause right here and go give the exercise a try. Now that you've had a chance to go through the project yourself, So pause right here and go give the exercise a try. Now that you've had a chance to go through the project yourself, Let's run through it. Exercise a - Variables, Part 1. Make a new project named "variables" using cargo. That's easy! cargo new variables Open Cargo.toml I'm going to go into the variables project and open it IntelliJ. Now I'll open Cargo.toml. Rust was indeed able to figure out my email address. Change the version number to 2.3.4 just for fun and save the file. Done. Now over to src/main.rs. Declare the variable missiles and initialize it to 8 Declare the variable ready and initialize it to 2. I'm using the implicit typing here. Explicitly I could put i32, which would be the default. It would be the same. I'll just leave that. Change the println! at the end of main to Firing blank of my blank missiles. Ready is the number and missiles is how many we have. Save. Run your program using cargo. Okay, let's go give it a try. cargo run. Firing two of my eight missiles. Perfect. Now what common problems could you have run into? One is forgetting a semicolon. That would look like this. Help: add a semicolon here. Another one is forgetting the let. Missile is not found in the scope because you're not declaring it. You're trying to use it. So let's declare it. All right, onward! Part 2. After the first print line, subtract ready from missiles like this. Okay, I'll just copy and paste that straight in. Missiles equals missiles minus ready. Add a second println! to the end. I will copy and paste that as well. We can already see there's an error here, but I'm going to run into it so that we can explain it. So run your program. cargo run. Cannot assign twice to immutable variable missiles. Okay, so missiles on line 2 is immutable. Let's go fix that. A mutable missiles. Now we should be able to reassign it. clear. cargo run. There we go. Six missiles left. Next, declare a constant named STARTING_MISSILES and set it to 8. const STARTING_MISSILES, screaming-snake-case, the type we need to set explicitly in a const, let's set it to 8. Declare a constant named READY_AMOUNT and set it to 2. READY_AMOUNT Declare a constant named READY_AMOUNT and set it to 2. READY_AMOUNT also an i32 set it to 2. Now we'll use those in place of our magic numbers. All right. Now we're using constants. Do they work? Yes they do. Same output. Okay. Where did you put the constants? If you put them in main, try moving them up above main at module scope. If we're going to use constants, we might as well make them available everywhere. Save. Now we've moved them up. They'll be available at any function in this module. cargo run. No change. All right. Now that we've done all the main exercises I'll leave the challenge up to you. 8. Functions: Let's talk about functions. You've seen the main function quite a bit already. Let's talk about functions in general. Functions are defined using the "fn" keyword fn is pronounced "fun". The Rust style guide says to use snake-case for function names: lowercase words separated by underscores. One awesome thing about Rust is that functions don't have to appear in the file before code that calls them. So feel free to leave main at the top of your main.rs file if you like. Hurray for more chronologically-arranged source code! Systems programmers can have nice things too. Function parameters are always defined with "name: type", and, of course, multiple parameters are separated by a comma. You specify the return type after the parameters by adding an arrow pointing to the return type. The arrow is a hyphen and then a greater-than symbol, and the body of a function is inside a block. You can return a value from a function using the return keyword as you would expect. There is also a shorthand for returning values. If you leave the semicolon off of the last expression in a block then it will be returned as the value of the block. This is called a "tail expression". In other words this is the same as this. Whenever you are returning something at the end of the block, the shorter way is preferred in idiomatic Rust. Calling a function looks the same as in most other languages. There's currently no support for named arguments at the call site so you need to provide all the values in the correct order. Finally, a single Rust function does not support variable numbers of arguments or different types for the same argument, but macros such as println do. A macro call looks just like a function call except the name of a macro always ends with an exclamation mark. We will use a couple common macros but I won't be teaching you how to create them anytime soon. That's a more advanced topic. In the next video we'll go over the module system. 9. Exercise B - Functions: Time for Exercise B- Functions. Let's go into the exercises b-functions exercise directory. Now let's open this up in an editor. I'll use in IntelliJ. You can use VSCode or whatever your preferred editor is. I've included directions for the exercise as comments inside of the main.rs file, so I encourage you to go and give this exercise a try, follow the directions and see how far you can get. Hopefully, you can get it all done. If you're able to do it all, feel free to skip this video and go on to the next section. If you have trouble or if you'd like to see how I did it, then come right back to this spot in the video and we'll go through it. All right, now that you've had a chance to do the exercise yourself, let's run through it. Number one. Try running this code with cargo run and take a look at the error. Can do! cargo run. Okay, cannot find value area in this scope. So we've got a scope problem here on line 15. Let's go take a look. Line 15, area is area. Okay, area is our variable. Oh look, it's defined in this inner scope, it gets dropped right here it's not available. In this case let's just get rid of this useless inner scope, save that. Now, if we clear and cargo run, area is 0. Alright, two: The area that was calculated is not correct. Go fix the area_of function below, then run the code again and make sure it worked. You should get an area of 28. Okay. So, let's go find the code. area_of, 2a. Fix this function to correctly compute the area of a rectangle given dimensions x and y by multiplying x and y returning the result. Okay. So we've got an area_of function, it takes an x, which is an integer, a y, which is an integer, and returns an integer that represents the area. it takes an x, which is an integer, a y, which is an integer, and returns an integer that represents the area. Easy enough! Just go x times y right here, we should be all good to go. cargo run again. Area is 28. Great. Challenge: The previous line is not idiomatic. Run cargo clippy. Figure out what's wrong and fix it. Okay, I'm gonna leave this challenge to you! I encourage you to do it, because this is not idiomatic. We're not using a tail expression, so go fix that! Number 3: uncomment the line below. It doesn't work yet, because the volume function doesn't exist. Create the volume function. It should take three arguments, multiply the three arguments together, and return the result which should be two hundred and eighty. Okay. If we get stuck, remember this is very similar to area_of. Okay I don't think we're going to need that, but let's go ahead and uncomment this. Go function volume. If I spell it correctly that would help. All right. It needs a width, height, and depth so we'll say x is an i32 and Y is an i32 and z is an i32. What does it return? An i32. Real diversity of types here. Okay, x times y times z. We'll just leave leave that as our tail expression, and cargo run. Volume is 280. Great! That's it for this exercise. 10. Module System: Rust has a powerful and flexible module system. Let's use this "hello" project as an example. Let's add a root library module by creating a lib.rs file. We already know that main.rs is a special file that will be the hello binary. lib.rs is a special file that will be the root of the hello library. Let's open up lib.rs and put a function inside it. Here is a pretty simple function. It's the only contents of the library. Let's go split screen and look at main.rs. The main function attempts to call the library's greet function directly by specifying the absolute path to the function. The absolute path is the library name, which is the same as the name of your project in Cargo.toml, The absolute path is the library name, which is the same as the name of your project in Cargo.toml, Hello, in this case, then the scope operator, which is double colons, and then the name of the function: greet. This almost works, but all items in a library are private by default, even to binaries in the same project. Let's add pub in front of the greet function to make it public. Now the code works, but specifying the absolute path of something at every call site could be really painful, especially if the path is really long, which is where the use statement comes in. Use brings an item from some path into some scope and I will often call it import without thinking about it because I'm so used to Python. So let's go back to our example and add a use statement above the main function. Since the use statement is outside of any smaller scope this brings greet into scope for all of main. Now we can simplify the function call to just greet. This works exactly the same as before, but it's more concise at the calling site. This use statement is how you'll bring into scope anything from the standard library or from any other project that you want to use. The standard library is always available by default. It's spelled "s t d" but when you read it out loud you say "standard". So, this is "standard collections hash map". You will become really familiar with the contents of the standard library, but until you do let me show you a shortcut for how to find documentation for things in the standard library. Go to Google and type in "rust std" and then the thing you want to find. For example if I wanted to know about the vector type I would put "rust std vector". You'll find that the top result is nearly always the page for the standard library item that you need. That's pretty cool. I use it all the time still. But what if you need something that's not in the standard library? crates.io is Rust's package registry. That's where I would start. For today's purposes you can consider crate to be a synonym for package. Most of the Rust community will use crate and package interchangeably. With that in mind, the named crate style makes sense for a package registry, but since crate technically has multiple meanings, and I want to be precise in my teaching, I'll mostly say package. Once you have identified the name and version of the package that you want to use, you need to go back to Cargo.toml and add the package as a dependency. We're going to do this in the dependency section that we skipped earlier. The format is the name of the package, then an equal sign, and then the version of the package in quotation marks. Here I've specified the rand package to use for generating random numbers. Now that I have it listed in my dependencies I can use it from both my library and my binary either by an absolute path or by bringing a specific item into scope with a use statement. In the next video we will go over Scalar Types. 11. Scalar Types: There are four scalar types. Integers, floats, booleans and characters. Let's go over integer types first. There are a lot of them. Unsigned integers start with u, followed by the number of bits the integer has, consistent across all platforms, except for usize. usize is the size of the platform's pointer type and can represent every memory address in the process. It's also the type you will usually use to index into an array or vector. Signed integers are the exact same story, except they use i for their prefix. i for integer, I presume. isize also has the same number of bits as the platforms pointer type. The maximum isize value is the upper bound of object and array size. This ensures that isize can be used to calculate differences between pointers and be able to address every byte within a value like a struct. If you don't annotate an integer literal then it defaults to i32 because it's generally the fastest integer even on 64-bit architectures. Now, just because the types have the same number of bits on all architectures doesn't mean all types are supported on all architectures. A 16-bit microcontroller might only support these types. Integer literals can be specified in a number of ways. Decimal is as you would expect. Hexadecimal begins with zero x. Octal begins with zero o. Binary begins with zero b, and a single u8 byte can optionally be specified by b- single-quotes enclosing a UTF-8 character in the ASCII range. This is pretty rare to use in my experience. Most people just use a decimal integer between 0 and 255. The terms u8 and byte are used interchangeably in Rust. You'll hear byte all the time instead of u8. The representations that take more than one digit can have any number of ignored underscores inside them or at the end of them. This is just for convenience and readability. For example, these are the same numbers with some underscores in places that we usually like, but you could also do this if you wanted to. The point is the underscores are ignored. Floating point types are much simpler. f32 has 32 bits of precision and f64 has 64 bits of precision. f64 is the default because it has more precision, but it can be really slow on less than 64- bit architectures, so be careful with that. Floating point literals follow the IEEE-754 standard but basically look like this. No special suffix is required, but you do always need to have at least one digit before the dot. So this is not a valid floating point literal, but this is because it has a digit in front of the dot. Numerical literals can optionally include the type as a suffix. Usually when you want a specific type, you will annotate a variable declaration and the type of the literal will be inferred, but it is also completely fine to suffix the literal with the type you want. This is especially useful if you want to pass a literal to a generic function that could accept multiple numeric types. This is one situation where underscores can really improve readability. Now for the boolean type. The actual type is specified with bool that's easy enough. The two boolean liberals are true and false, all lowercase. Booleans are not integers, so don't try to use arithmetic on them, it won't work, unless you cast them to some integer type like this. The character type is misnamed. Even though you specify it with care it actually represents a single unicode scalar value which could be anything from a character of our alphabet to a character of someone else's alphabet to an ideograph or a diacritic or an emoji or a non-printable Unicode control character that could represent anything from a sound to an action. A character is always 4 bytes which effectively makes an array of characters a USC-4 or UTF-32 string. Character literals are specified using single quotes, and most important of all, characters are fairly useless. Strings are UTF 8 and characters are not, so strings do not use characters internally. Source files are also UTF-8, so chances are when you want to deal with a single character it's going to be a UTF-8 string not a character literal. We'll talk about strings in a later video. In the next video we'll talk about compound types. 12. Compound Types: Compound types gather multiple values of other types into one type. The first compound type is the tuple. Tuples store multiple values of any type. We've seen it before. It looks like this: parentheses containing comma separated values. The type annotation is pretty intuitive: parentheses surrounding comma-separated types. There are two ways to access members of a tuple. The first way is to use dot syntax, also known as a field access expression. I believe Rust uses the dot syntax here instead of square brackets to emphasize that members of tuples are not always the same type. Since a tuple's fields have no names, you use their indices, starting with 0. The second way to access members of a tuple is all at once. We've seen this before. You can use a pattern to destructure and access all the elements of a tuple. Please be aware that tuples currently have a maximum arity of twelve above which you can technically still use the tuple, but only with limited functionality. Arity means how many items are in the tuple. So this tuple type has an arity of 4. Since the current maximum arity is twelve, you can have a dozen elements in a tuple, but not a baker's dozen. At least not with full functionality. Arrays by contrast, store multiple values of the same type. You can specify them literally with square brackets and commas or with a value and how many you want separated by a semicolon. Note that the type annotation for an array always uses the semicolon form. Even when you specify all the literal values in the array. You index values in an array as you would expect with square brackets. What you would not expect however is that arrays are limited to a size of 32, above which they lose most of their functionality. Arrays live on the stack by default and are fixed size, so you will usually use vectors or slices of vectors instead of arrays. We'll talk about vectors in a later video. In the next video we will talk about control flow. 13. Exercise C - Simple Types: Time for Exercise C. Time for Exercise C. We'll be going over simple types. I encourage you to go ahead and do this project now. Once you're done come back and we'll continue and go over the answers. All right now that you've had a chance to do these exercises yourself, let's walk through it. Number 1: Pass parts of coords to the print difference function. This should show the difference between the two numbers in coords when you do cargo run. Use tuple indexing. The print_difference function is defined below the main function. It may help if you look at how it is defined. All right. Let's uncomment this line. Let's uncomment this line. print_difference. If we scroll down, we see that it takes an x and a y, which are f32s. Come back to the top. Coords is a tuple of two f32s. So we simply need to extract these two and pass them to the function individually. So we'll use our tuple indexing. So coords.0, coords.1. Save. cargo run. Great! Difference between 6.3 and 15 is 8.7. Number two - we want to use the print_array function to print coords, but coords isn't an array. Create an array of type [f32; 2] and initialize it to contain the information from coords. Uncomment the print_array line and run the code. Once again we've got some code to uncomment. So here's our new array. Let's make an array literal, and we'll put it in again, once again, coords.0 coords.1. That should work just fine. If you wanted to specify the type explicitly, we would say What's the type? f32. Semicolon. How many do we want? Two, so two f32's. Save that. clear. cargo run. Now I've got: The coordinates are (6.3, 15) The function took that value just fine. Next! Number three. We have this series here, which is an array of i32's. Make the ding function happy by passing it the value 13, it's right there, out of the series array use array indexing. Done correctly, cargo run will produce the additional output: Ding, you found 13! So let's do that. So series is our array. We'll use our square brackets to index into the array, and so we'll get zero, one, two, three, four, five, six! Put in six, save. Clear and run. Okay! Ding, you found 13! Number four: Pass the on_off function the value true from the variable mess. Oh, so we got this mess up here and here's the true. Done correctly cargo run will produce the additional output: Lights are on! I'll get you started. Okay. So here's the on_off call. There's mess, which is a tuple. So we've got ".2". So here's zero, one, two. So two is that array. Let's use array indexing to get zero, one. That'll get us this tuple here. So array indexing, one. Now we're down to this. Now we've got a tuple again. We'll use tuple indexing and get the zeroth element out of it. So that's a .0, semicolon. Save. Come give it a try Okay. Lights are on. All right. Five. What a mess -- functions in a binary! Let's get organized! Make a library file. So that src/lib.rs Move all the functions into the library. Make them all public and bring all the functions into scope using use statements. Okay. Remember the name of the library is defined in Cargo.toml You'll need to know that to use it. All right. So just because this is in the c-simple-types directory doesn't mean that's necessarily the name of our project. I mean it could be! But let's go take a look. So if I open up my sidebar here and go to Cargo.toml -- Ah! ding_machine is the name of our project. So let's go back to main. Let's make our lib.rs next. So inside of src, we go new rust file. lib.rs. Now that we've got our library file, let's go put our functions in it. So back to main. Grab all of our functions. Cut. Go over to library. Paste. Now, these are all private by default, so we can't use them yet, So let's add pub to all these functions. All right. Now they're all public. Let's go back over to main. Let's close the sidebar. We need to bring those all into scope. So, use, the name of our project and hence the name of our library is ding_machine. Our scope operator, and then here's our different things we could use. So yes, we want on_off, and then let's just do them all here. We want on_off, we want print_distance. We want print_array. We want print_difference. Did we miss any? Ah, ding. We want ding. Let's see if we got them all now. Okay. we got them all now. Okay. Looks like we've got them all. So let's go run this real quick and make sure it still works! Clear, cargo run. Ah, a warning. What is this? Okay, unused import: print_distance unused import: print_distance print_distance is used by the challenge, so I will leave that up to you to go and use that and get rid of this warning. 14. Control Flow: Control flow is awesome in Rust. "if" expressions are great. You don't need parentheses around the condition, because everything between the "if" and the opening curly brace is the condition. The condition must evaluate to a boolean, because Rust does not like type coercion and mostly doesn't coerce types. If you want to chain a condition, you use "else" and "if" separately, and of course you can finish with "else". "if" is an expression, not a statement. Statements don't return values, expressions do, meaning that we can change this code to this: message is assigned the value of the "if" expression. Four things to note: Number one, there are no semicolons after the branch values to make it so that the values get returned from the blocks as tail expressions. Two: we can't use return for this purpose, even if we wanted to, because return only applies to blocks that are function bodies, so return would return out of the current function. Three: all the blocks return the same type. Rust is strongly typed, and Four: there is a semicolon at the end of the if expression. If you don't use the value of an if expression then Rust will let you cheat and leave off the semicolon, but if you do use the value of an if expression in a statement, then you need to put a semicolon after it before starting any other statements in the block. The braces are not optional, by the way. You always have to have them, which means you never hit that terrible situation that you do in C where you mean to add a second line to the branch of an if statement, but instead your second line ends up outside the branch body and happens unconditionally despite your careful indentation. Isn't that just the worst? Another pain point we avoid is the confusing ternary operator in C. Yes, it's short and concise, but it's hard to read even for a simple expression like this, and can get downright ugly if you nest it. I mean, it's not clear to me what's going on here without careful mental parsing. Even nicely formatted it takes some experience to understand what the heck is going on. In Rust, since if is an expression you can just write it out like this. It's pretty obvious what's going on just by reading it and the nested version is still readable. Let's move on to the unconditional loop. It turns out that if the compiler knows a loop is unconditional there's some pretty cool optimizations it can do. Of course, even unconditional loops need to end eventually, so you use the break statement for that, but wait there's more! What if you want to break out of a nested loop? Can do! First, annotate the loop you want to break out of with a label. This one is named bob. Labels have a strange syntax: single apostrophe beginning an identifier. Some people say tick identifier. Then, tell break which loop you want to break out of. When this code hits the break statement in the inner loop, it will break all the way out of the outermost loop. Continue is very similar. By itself it continues the inner most loop. If you give it a label, then it continues the named loop. "while" loops have all the same behaviors as unconditional loops, If you give it a label, then it continues the named loop. "while" loops have all the same behaviors as unconditional loops, except they also terminate the loop when their condition evaluates to false. That is, the exact boolean value of false. Remember, Rust refuses to coerce expressions to booleans. You know, if you think about it while loops are pretty much just syntactic sugar for putting a negated break condition at the top of an unconditional loop. There is no "do while" construct in Rust, but it's pretty easy to rearrange this code to make it happen. Just move the condition to the bottom of the loop. Voila! "do while." Similar to modern scripting languages, Rust's "for" loop iterates over any iterable value. Compound and collection types will typically have a few different ways to get an iterable value for them. The iter method is one of the most common ways to get an iterator. The iterator you use determines which items are returned and the order that they are returned in. iter iterates over all items in a collection in order if the collection is ordered, and randomly if the collection is unordered. If you're a fan of the functional programming style, you'll be happy to hear that you can stack methods like map, filter and fold and they will be lazily evaluated. As an added bonus, the for loop can take a pattern to destructure the items it receives and bind the inside parts to variables, just like the let statement. Only in this case, the variables are local to the body of the for loop. Systems programmers can have nice things too! Only in this case, the variables are local to the body of the for loop. Systems programmers can have nice things too! You'll probably want to use ranges with for loops at some point. The syntax for a range is two dots separating your start and end points. The start is inclusive and the end is exclusive. So this will count from zero to forty nine, and if you use dot dot equal then the end will be inclusive as well. So this will count from zero to 50. In the next video I'm going to talk about strings. 15. Strings: Strings. I'm going to warn you up front: here be dragons! I'll do my best to steer you right. There are at least six types of strings in the Rust standard library. But we mostly care about two of them that overlap each other. The first is called a string slice and you will almost always see it as a borrowed string slice. We'll talk more about borrowing later. A literal string is always a borrowed string slice. A borrowed string slice is often referred to as a string which can be really confusing when you learn that the other string type is a string with a capital S. The biggest difference between the two is that the data in a borrowed string slice cannot be modified while the data in a string can be modified. You will often create a string by calling the to_string() method on a borrowed string slice or by passing a borrowed string slice to String::from. A borrowed string slice is internally made up of a pointer to some bytes and a length. A string is made up of a pointer to some bytes, a length, and a capacity that may be higher than what is currently being used. In other words a borrowed string slice is a subset of a string in more ways than one. Which is why they share a bunch of other characteristics. For example both string types are valid UTF-8 by definition, by compiler enforcement, and by runtime checks. Also strings cannot be indexed by character position. Why not? Because English is not the only language in the world! In fact, Google told me that there were over 6900 living languages, and emojis on top of that, and they all seem to make their way into Unicode, and strings are Unicode, which means things get complicated. Let's take a look at the Thai word sawatdee. Let's say that we wanted to get this thing at what we think should be index three. Ultimately this string is stored as a vector of 18 bytes. Would we get what we wanted if we indexed in by bytes? Not even close! Unicode scalars in UTF-8 can be represented by 1, 2, 3, or 4 bytes, and you have to traverse the bytes in order, to tell where one scalar ends and the next begins. In this case every three bytes is a Unicode scalar, so if there were a way to index into the scalars would we get what we want? Closer, but still off. Diacritics are Unicode scalars that combine with other Unicode scalars to produce a different grapheme, and the grapheme is usually what we care about. So now you understand that graphemes decompose into variable amounts of scalars, which decompose into variable amounts of bytes. As part of Rust's emphasis on speed, indexing operations on standard library collections are always guaranteed to be constant time operations. You can't do that with strings, because the bytes, which are indexable, aren't guaranteed to be what people want when they index into a string, and the graphemes, which people do want, can only be retrieved after slowly examining a sequence of bytes. So when presented with a string you have some options: you can use the bytes() method to access the vector of UTF-8 bytes which you can index into if you want since bytes are fixed size. This actually works fine for simple English text as long as you stick to the portion that overlaps ASCII. You can use the chars() method to retrieve it iterator that you can use to iterate through the Unicode scalars, and finally you can use a package like unicode-segmentation which provides handy functions that return iterators that handle graphemes of various types. With each of these approaches you know that if you can index into something, it will be a fast, constant-time operation while if you iterate through something it is going to process some variable number of bytes during each iteration of the loop. Hopefully, you can sidestep most of these issues by using one of the many helper methods created to manipulate strings, but if you do end up manually using one of the iterators, iterators have a handy method called nth() that you can use in place of indexing, and now you know why you have to pick an iterator and use nth() instead of being able to index into a string directly. In the next video we will talk about ownership. 16. Exercise D - Control Flow & Strings: It's time for Exercise D - Control flow and strings. First, you ought to go give these exercises a try by yourself and see if you can get all the way through them. First, you ought to go give these exercises a try by yourself and see if you can get all the way through them. If you can't get all the way through them or you'd like to see my take on them then come right back to this spot in the video and we'll go over them. Okay, now that you've had a chance to try these yourself, let's go over these exercises. All right. So first we're collecting our arguments into a vector of strings and then we're going to consume the arguments vector and iterate through each of our strings. All right. 1a - your task: handle the command line arguments. If arg is sum then call the sum function. If arg is double then call the double function. If arg is anything else, then call the count function passing arg to it. Hint: You will need to convert your string literals into a string before you'll be able to compare them with double equals. All right. If arg is sum, but converted to a string, then call the sum function. Else, if arg is equal to, this time let's do String::from("double") They do the exact same thing. This is two different ways to do it. I usually do the to_string(). Then, call double. Otherwise for any other condition we'll call count and pass it arg. All right, let's go give it a try. Doesn't do anything because we didn't give it an argument. How do you give your program an argument? Really easy. Just give it an argument. Now, you may collide with an actual argument of cargo. To make it explicit you can add a double dash, and now the arguments after it will not be interpreted by cargo. The sum is zero. You can double x 0 times before it is larger than 500, and anything else is blank right now. We'll fix that in a minute. 1b - Try passing sum, double, and bananas...okay we did that. Next, 2 - use a for loop to iterate through integers from 7 to 23 inclusive using a range and add them all together. Increment the sum variable. Hint: You should get 255. Run it with cargo run sum. All right. So, use a for loop. So for...what is our index variable? Let's do i in...now what's our range? Well, we need to go from 7 to 23 inclusive, so we can go 7..24, that's an exclusive range, so that'll end with 23. Or we could go ..=23 to use an inclusive range. Let's just do that in this case, and then we'll say...what do we do? Well, we take our mutable sum and we'll add i to it, and that should be it. That should loop through, it'll end at the end of the range and then we'll print the sum is, hopefully, 255. Let's go give it a try. cargo run sum. Sum is 255, great! 3 - use a while loop to count how many times you can double the value of x...multiply x by two... before it is larger than 500. Increment count each time through the loop. Run it with cargo run count. Hint: the answer is 9 times. Okay, so while loop...what's our condition? Well, before it is larger than 500, so x is smaller than 500. Okay. What we do? Increment count count plus equals 1 and then we've got a double x. So x times equals two. Now we can also do x = x * 2. We're just using the syntactic sugar here for the combined operation. Go give it a try, let's clear the screen, cargo run sum. The sum is...Oh! Sorry, this one is double. You can double x 9 times before it's larger than 500. Perfect! And then we're on to the challenge. Okay, I'll let you do this. This is the challenge! 17. Ownership: Let's talk about ownership. Ownership is actually why I chose to learn Rust in the first place. A few years ago I decided I really wanted to master a systems programming language. I don't know about you, but I don't have a lot of extra time in my day. So if I was going to master a systems programming language, I wanted to make sure it would be worth it. I spent a month researching all of the options and it really came down to three viable choices in the end. C or C++ both of which I had already had painful experiences with, or an up-and-coming new language called Rust. Rust's ownership model is the reason that I chose to spend my extra time focusing on Rust. Ownership is what makes those crazy safety guarantees possible and makes Rust so different from other systems programming languages. Ownership is what makes all those informative compiler error messages possible and necessary. There are three rules to ownership: First, each value has an owner. There is no value in memory no data that doesn't have a variable that owns it. Second there is only one owner of a value. No variables may share ownership. Other variables may borrow the value which we'll talk about in just a minute but only one variable owns it. Third when the owner goes out of scope, the value gets dropped immediately. Let's see ownership in action. Let's create a string s1 and then create another variable s2 and assign s1's value to it. What happens to this string is not a copy. At this point the value for S1 is moved to S2 because only one variable can own the value. If we try to go ahead and use s1 after this point we get a compiler error: borrow of moved value s1. So what's going on here? Well, we've got stack and heap sections of memory; just a super quick refresher: The stack stores values in order, which it can do because values are always fixed size, and the last value in is the first value out, which all tends to be very fast because it's so compact and predictable. The heap, in contrast, stores values all over the place, and values tend to be all sorts of sizes, and don't get dropped in any order that corresponds to when they were created, which tends to make using the heap slower than this stack because you're always hopping around memory, flushing and reloading your CPU's memory cache. What does that have to do with a value being moved? Let's walk through it. First, we create s1. A pointer a length and a capacity get pushed onto the stack. The value of capacity in this case is 3, the length is 3, and the pointer points to some newly allocated bytes on the heap. This altogether is the value of the string s1. Then we move s1's value to s2, because we can only have one owner. It works like this: the pointer, length, and capacity all get copied from s1 and pushed as new values on the stack as part of s2. If we stopped here, then memory safety would be non-existent, which is why Rust immediately invalidates s1. It still exists on the stack, the compiler just considers s1 now uninitialized and won't let you use it after this point. It's more than a shallow copy, it's a move, which is why we can't use s1 anymore. The value has moved to s2. Technically if s1 were mutable we could assign it some new value and then use it again. But since it is immutable in this example, it's just garbage and we can't use it anymore. What if we didn't want to move the value but copy it instead? To make a copy of s1 we would call the clone() method. Why is it called clone instead of copy? Let me explain what it does under the hood, and then I'll tell you. Clone performs the same initial copy of the stack, but then also copies the heap data and adjusts s2's pointer to point to it. In both the move and the clone situations the three rules are satisfied: 1) the values have owners, and 2) only one owner, and 3) when the variables go out of scope the values will be immediately dropped. The stack and heap data, if there is heap data, together make a value. Rust reserves the term copy for when only stack data is being copied. If there is heap data and pointer updates involved, then we use the term clone. In other languages you might call a clone a deep copy. When a value is dropped that means the destructor, if there is one, is immediately run, the heap portion is immediately freed, and the stack portion is immediately popped. So, no leaks, no dangling pointers, yes happy programmer! Another move situation: Let's start with the same string and make a function that takes a string and returns nothing. If we pass s1 to that function, s1 is moved into the local variable s in do_stuff(), which means we can't use s1 anymore, because it got moved! So what do we do? One option is to move it back when we're done. We'll just make s1 mutable, add a return type to do_stuff(), and then return s as the tail expression, which gets moved back out of the function and used to reinitialize s1, but that's usually not the pattern that you want! Passing ownership of a value to a function usually means a function is going to consume the passed in value. For most other cases, you should use references, which is why it's time to talk about references and borrowing in our next video. 18. References & Borrowing: Now that we've gone over ownership, we can talk about references and borrowing. Instead of moving our variable, let's use a reference. Here's our do_stuff() function again, only this time it takes a reference to a string. The ampersand before that type indicates a reference to a type. When we call do_stuff() we pass it a reference to s1, and s1 retains ownership of the value. do_stuff() borrows a reference to the value. The reference, not the value, gets moved into the function. At the end of the function the reference goes out of scope, and the reference gets dropped, so our borrowing ends at that point. After the function call, we can use s1 like normal, because the value never moved. Under the hood, when we create a reference to s1, Rust creates a pointer to s1, but you will almost never talk about pointers in Rust because the language automatically handles their creation and destruction for the most part, and makes sure they're always valid using a concept called lifetimes. Lifetimes can be summed up as a rule that references must always be valid, which means the compiler won't let you create a reference to outlives the data it is ultimately referencing, and you can never point to null. References default to immutable, even if the value being referenced is mutable, but if we make a mutable reference to a mutable value then we can use the reference to change the value as well. The syntax for a mutable reference is a little special: ampersand, mute, space, variable or type. Now wait, why didn't we have to dereference the mutable reference in order to alter s in the do_stuff() method? Look at this: we are using the same dot syntax to access a string method on a mutable reference as we do for the value itself. How does that work? The dot operator for a method or a field auto-dereferences down to the actual value. So at least when you're dealing with the dot operator you don't have to worry about whether something is a value or a reference or even a reference of a reference. If we manually dereferenced s, it would look like this: you use an asterisk immediately before a reference to dereference to the value, similar to C. The dereference operator has pretty low precedence, so you'll sometimes need to use parentheses. With most other operators, like the assignment operator for example you'll need to manually dereference your reference if you want to read from or write to the actual value. Here I'm dereferencing s so that I can replace the entire value. So, let's stop and go over what references look like again. If you have a variable X, then this creates an immutable reference to that variable's value, and this creates a mutable reference to that variable's value. Similarly with types, if this is the type of your value, then this is the type of your immutable reference, and this is the type of your mutable reference. Going the other way around, if your variable is a mutable reference to a value, then dereferencing x gives you mutable access to the value, and if x is an immutable reference to a value then dereferencing x gives you immutable access to the value. Naturally, since references are implemented via pointers, Rust has a special rule to keep us safe. At any given time you can have either exactly one mutable reference or any number of immutable references. This rule applies across all threads. When you consider that references to a variable may exist in different threads, it starts to make it pretty obvious why it's not safe to have multiple mutable references to the same variable at the same time without some type of locking, but if all the references are immutable then there's no problem. So you can have lots of immutable references spread across multiple threads. All these rules I've been talking about are enforced by the compiler, and by enforced I mean compiler errors, lots of compiler errors! At first you're like Aaargh! I hate the compiler! It keeps giving me errors! But then as you get the hang of it you realize you don't get mysterious segfaults anymore, and the error messages are really pretty informative! And if the code compiles, it works! And that is an amazing feeling! In the next video we'll learn about Structs. 19. Exercise E - Ownership & References: Exercise E - Ownership and references. Once again, I encourage you to try to do this exercise by yourself and then come back. All right. Now that you've had a chance to do this yourself, let's run through it. This code either gets the first argument as a string or prints usage and exits if an argument wasn't provided. Next, we've got: write a function, inspect, that takes a reference to a string, returns nothing, but prints whether the contents of the string is plural or singular. So, the point of this function is to take a reference to a string so that we can look at this string and then print something out. I'm going to go ahead and make this function right here inline. This is bad style. Don't do this in real life. We'll move it down to the bottom later. Our function is named inspect and it takes a reference to a string. I'll just call that s. The function doesn't return anything, so we can go straight to the function body. We know we're going to print something, so let's add a print line. Let's make this one the plural print line, and then we'll copy it and change it to singular. For those to work, we need to pass our string reference s to both print lines. The point of the function is to print out either one or the other, so let's add our if statement and check to see if s ends with "s". If it does, then let's print out that it's plural. If it doesn't, we'll print out that it's singular. So our inspect method takes s, which is a reference to a string. It then calls s.ends_with. The dot dereferences down to the value and calls the ends_with method on the value. We pass it a borrowed string slice "s". and then if it does end with s we print out it's plural, otherwise we print out it's singular. Let's give it a try. If we do "cargo run -- apple" we should see "apple is singular." Great. If we add an s, then we'll see "apples is plural." Now I'm going to clean up after myself real quick by putting this function down outside of main. Now let's do number two. I'm going to uncomment this code, and then we'll write a function that takes a mutable reference to a string. The function is called change. I'll call the argument s, again. Our task this time is to add an s to the end of the word if it doesn't already have it. This is going to begin really similarly to our last function. We're going to use s.ends_with("s"), only this time we care about if it doesn't end with s. If it doesn't, then we'll use the push_str method to add an s to the string. This only works if we have a mutable string, and we have a mutable reference to a mutable string, so this will work. Now that I have this written, let's move it down to the bottom of the file where it ought to go. Let's go try it out with "cargo run -- apples". There's our new output. In this case we passed in apples and it came through unchanged. If I pass in just apple, we can see that our function adds an s and still prints out the same thing. All right let's go back and do number three. First, let's uncomment this code, and then we'll make a function called eat. Eat is going to take a string by value, which means it's going to consume it. That means that the variable passed into this function won't be usable anymore after the function call. That means that the variable passed into this function won't be usable anymore after the function call. This function returns a boolean. To determine the return value we need to figure out if this string starts with a "b" and contains an "a". If it does, then we want to return true. Otherwise we return false. Let's go try this out. If "eat" returns true If "eat" returns true We should see "Might be bananas." If it returns false we should see "Not bananas." First, let's try it with apple: not bananas. Great. Now let's try it with boats. Okay, might be bananas. Let's take another look at this "if expression." We can simplify this. We're returning true for true and false for false. So let's just return the condition itself. Doing a quick spot check with boats assures us that it's still working. Our next instructions say try running this program with boat, banana, and grapes. First, boat. Okay, boat is singular. Many boats. Might be bananas. Okay, then we'll change it to banana. Okay, then we'll change it to banana. Singular. Many bananas. Might be bananas. And then we'll do our grapes, and then we get: grapes is plural. I have many grapes. Not bananas. Okay so everything works. I'll leave the final portion of the exercise, the challenge, to you! 20. Structs: It's time to learn about structs. In other languages you have classes. In Rust you have structs. Structs can have data fields, methods, and associated functions. The syntax for the struct and its fields is the keyword struct, then the name of the struct in capital-camel case, RedFox in this case, then curly braces, and then the fields and their type annotations in a list separated by commas, and as a nice touch you can end your last field with a comma as well, so the compiler doesn't yell at you when you add another field and don't think to add a comma to the field before it. This feature is repeated in lots of Rust constructs that use commas. Instantiating a struct is straightforward, though verbose. You need to specify a value for every single field. Typically, you would implement an associated function to use as a constructor to create a struct with default values, and then call that. Methods and associated functions are defined in an implementation block that is separate from the struct definition. The implementation block starts with impl, and then the name of the struct whose functions and methods you're going to implement. This is an associated function because it doesn't have a form of self as its first parameter. In many other languages you would call this a class method. Associated functions are often used like constructors in other languages, with new being the conventional name to use when you want to create a new struct with default values. You'll note that Self with a capital S can be used in place of the struct name inside the implementation block. You can also just type out the struct name if you want to but I recommend using Self instead. Let's create our RedFox like this. The scope operator in Rust is double colons, and we use it to access parts of namespace-like things. We've already used the scope operator in use statements to access items inside of modules. Here we're using it to access an associated function of a struct. Once you have an instantiated value you get and set fields and call methods with dot syntax as you do in most languages. Methods are also defined in the implementation block. Methods always take some form of self as their first argument. At this point in the tutorial for most other languages I would go over class inheritance for a little bit, or struct inheritance since that's what Rust calls its classes. It's a short discussion though, because there is no struct inheritance. So wait is Rust an object-oriented language or not? Well, it turns out that's a religious-war question because there is no universally-accepted definition of what an object-oriented language is or isn't, and since the Rust community doesn't do religious wars nobody really cares what the answer is. The real question is: Why doesn't Rust have struct inheritance? The answer is because they chose a better way to solve the problem we wish inheritance solved: Traits, which is what we are going to talk about in the next video. 21. Traits: Traits. Traits are similar to interfaces in other languages and they're really cool. Rust takes the composition over inheritance approach. Remember our RedFox struct? Let's make a trait called Noisy. Traits define required behavior. In other words, functions and methods that a struct must implement if it wants to have that trait. The Noisy trait specifies that the struct must have a method named get_noise() that returns a borrowed string slice if the destruct wants to be noisy. So, let's add an implementation of the Noisy trait for RedFox. Let's take a look at how this works. We implement the Noisy trait for the RedFox struct and our implementation of the required get_noise() method is "meow," which is all great, but why bother? We could have just implemented that method for RedFox directly without involving a trait at all. The answer is that once we have a trait involved we can start writing generic functions that accept any value that implements the trait. This function takes an item of type T which is defined to be anything that implements the Noisy trait. The function can use any behavior on item that the Noisy trait defines. So now we have a generic function that can take any type as long as it satisfies the Noisy trait. That's pretty cool. As long as one of either the trait or the struct is defined in your project you can implement any trait for any struct. That means you can implement your traits on any types from anywhere including built-ins or types you import from some other package. And on your struct you can implement any trait whether built-in or from some project. Here I've implemented the Noisy trait for the built-in type u8, which is a byte, so our generic print_noise() function works just fine with bytes now. There is a special trait called Copy. If your type implements Copy, then it will be copied instead of moved in move situations. This makes sense for small values that fit entirely on the stack, which is why the simple primitive types like integers floats and booleans implement Copy. If a type uses the heap at all then it cannot implement Copy. You can opt-in to implementing Copy with your own type if your type only uses other Copy types. Okay, let's go through an example of where traits really seem to shine. Let's say we have a game with a goose, a Pegasus, and a horse. These have several attributes in common: These two can fly. These two can be ridden, and these two explode. You can see how it could get really tricky to use object inheritance to define the correct behavior that each final object needs to have. But it's pretty straightforward with traits. The goose implements the fly and explode traits, the Pegasus implements the fly and ride traits, and the horse implements the ride and explode traits and we're all happy. It's interesting to note that traits implement inheritance so a trait can inherit from another trait. That makes it possible to have a trait inheritance hierarchy like this, where movement is the root trait, run inherits from movement, and so on. Let's say we have a separate damage trait hierarchy with a damage parent trait and an explode child trait. Then a horse struct that implements ride and explode also has to implement the parent traits: movement, run, and damage. Making your trait inherit from the parent trait really just means that anyone who implements your trait is going to have to implement the parent traits as well. Traits can also have default behaviors, so if you design structs and traits carefully enough, you might not have to implement some of the trait methods at all! If you want to implement default trait behavior, inside your trait definition instead of ending your function or method definition with a semicolon, add a block with your default behavior. Then when you implement the trait for your struct just don't provide a new definition for the method whose default implementation you want to use. The presence of an implementation will override the default. This is a fully functional example: The Robot struct in the middle implements the Run trait, but it doesn't override the default run() method. So when robot.run() is called from main() at the bottom, it executes the default run() method defined on the trait at the top and prints: "I'm running." One gotcha I really ought to mention: you can't define fields as part of traits. That may be added in the future if someone can figure out the right way to do it. In the meantime the workaround is to define setter and getter methods in your trait. In the next video we will go over some of the collections in the standard library. 22. Exercise F - Structs & Traits: Exercise F: Structs and Traits. As always I encourage you to go give this exercise a try yourself. If you are going to do that, now is the time to pause! Number one: define a trait named Bite. Okay. Trait named Bite. Define a single required method. fn bite() which takes a self which is a mutable reference to myself, and that's all we need in the trait. We will call this method when we want to bite something. Once this trait is defined you should be able to run the program with cargo run without any errors. Let's give that a try. All right. I take a bite. Carrot is now { percent_left: 80 } Carrot is now { percent_left: 80 } Number two: now create a struct named Grapes with a field that tracks how many grapes are left. If you need a hint, look at how it was done for carrot at the bottom of this file. This is going to make it so that it can be printed out. We're just going to derive the debug trait for us. Our struct Grapes. A field that tracks how many grapes are left. Well, let's just call it the amount left, and what is it? How about an i32? If we run this nothing's going to happen except it shouldn't crash. Let's make sure it doesn't crash. Did not crash, great. Implement Bite for Grapes. Ok, impl Bite for Grapes. When you bite a Grapes subtract one from how many grapes are left. Okay. So we need that function bite there. So let's copy and paste that. So we've got a mutable reference to our self. We know that we've gotten an amount left. So let's say self.amount_left minus equals one. Don't have anything to return, so we're all done. Into main()! All right. Number four: uncomment and adjust the code below to match how you defined your Grapes struct. Okay, amount_left, amount_left we're good there. So we're going to create a Grapes with 100 grapes. Then we're gonna grapes.bite() and we should see "Eat a grape:" and it should show our grape representation here with 99. So let's save. Clear. Cargo run. "Eat a grape: Grapes { amount_left: 99}" Good stuff. Last we have the challenge, which I will leave up to you! 23. Collections: Let's go over some collections. These are all in the standard library. Vector is a generic collection that holds a bunch of one type, and is useful where you would use lists or arrays and other languages. It's the most commonly used collection by far. When you create a vector you specify the one type of object that it will store an angle brackets. Once you have the vector, you can push values into it. Vectors act like a stack, so push appends things to the end, and pop removes the item at the end of the vector and returns it. Since vectors store objects of known size next to each other in memory, you can index into it. If your index is out of bounds, Rust will panic, which is much more helpful than feeding you garbage! So check your length before you index. Quite often you know the values of a vector when you create it. There is a macro called "vec!" with a lower-case v that makes creating vectors from literal values much more ergonomic. Vectors give you a lot of great low-level control over their behavior, and they have a ton of methods to do just about anything you could possibly want: insert, remove, split, splice, sort, repeat, binary search -- it's all in the standard library. HashMap is a generic collection where you specify a type for the key and a type for the value, and you access the values by key. In some languages you would call it a dictionary. The whole point of a hash map is to be able to insert, look up, and remove values by key in constant time. When you create a hash map, you specify the type of the key and the type of the value. In this case, the key is a byte and the value is a boolean. Once you have a hash map you insert entries with the insert() method. Use the remove() method to get a value. remove() actually returns an enum called Option. We will talk about enums in the next video. There are also methods for getting references to values and iterating through keys, values, or keys and values, either as immutable or mutable references. There are a bunch of other collections that I will just quickly describe. VecDeque uses a ring buffer to implement a double-ended queue, which can efficiently add or remove items from the front and the back, but everything else is a little less efficient than a regular Vector. LinkedList has the dubious distinction of being quick at adding or removing items at an arbitrary point in the LinkedList has the dubious distinction of being quick at adding or removing items at an arbitrary point in the list, but slow doing absolutely anything else. HashSet is a hashing implementation of a set that performs set operations really efficiently. BinaryHeap is like a priority queue which always pops off the max value. BTreeMap and BTreeSet are alternate map and set implementations using a modified binary tree. You usually only choose these over the hash variants if you need the map keys or set values to always be sorted. In the next video we'll talk about in enums. 24. Enums: Now it's time to talk about enums. Enums in Rust are more like algebraic data types in Haskell than C-like enums. You specify an enum with a keyword enum, the name of the enum in capital camel-case, and the names of the variants in a block. You can stop there if you want, and just use it like that, in which case it is sort of like an enum in C. Just namespace into the enum and away you go. However the real power of a Rust enum comes from associating data and methods with the variants. You can always have a named variant with no data. A variant can have a single type of data, a tuple of data, or an anonymous struct of data. An enum is sort of like a union in C only so much better. If you create an enum the value can be any one of these variants. For example: your DispenserItem could be an Empty with no data associated with it, but you can tell that it's an Empty. Or it could be an Ammo with a single byte in it, or it could be a Things with a String and a signed 32-bit integer in it. Or it could be a Place with x and y i32s. It can be any one of those, but only one at a time. Even better, you can implement functions and methods for an enum. You can also use in enums with generics. Option is a generic enum in the standard library that you will use all the time. The T in angle brackets means any type. You don't have to use T, you could use some other valid identifier, but the idiomatic thing to do in Rust is to use T or some other capital letter. The Option enum represents when something is either absent or present. If you're trying to reach for a null or nil value like in other languages, you probably want to use an Option in Rust. You either have some value wrapped in the Some variant, or you have None. I'm going to use an Option for the rest of the examples in this section. Because enums can represent all sorts of data, you need to use patterns to examine them. If you want to check for a single variant, you use the "if let" expression. "if let" takes a pattern that will match one of the variants. If the pattern does match, then the condition is true and the variables inside the pattern are created for the scope of the "if let" block. If the pattern doesn't match then the condition is false. This is pretty handy if you care about one variant matching or not, but not as great if you need to handle all the variants at once. In that case, you use the match expression, which is match, a variable whose type supports matching, like an enum, the body of the match in braces, where you specify patterns followed by double arrows, which are equal signs followed by greater than symbols pointing to an expression that represents the return value of that arm of the match. Match expressions require you to write a branch arm for every possible outcome. In other words, the patterns in a match expression must be exhaustive. A single underscore all by itself is a pattern that matches anything and can be used for a default or anything-else branch. Note that even though you will often see blocks as the expression for a branch arm, any expression will do, including things like function calls and bare values. Either all branch arms need to return nothing or all branch arms need to return the same type. Remember that if you actually use the return value of an expression that ends in a curly brace like match, if let, or if, or a nested block, then you have to put a semicolon after the closing brace. If you don't use the return value of a braced expression then Rust lets you cheat and leave off the semicolon. If you don't use the return value of a braced expression then Rust lets you cheat and leave off the semicolon. I want to talk a bit more in depth about two special enums. What's so special about them is that they're used all over the standard library, so you will encounter them constantly. First, let's look a little more at Option. Here's the definition of Option again. As I said earlier, Option is used whenever something might be absent. Here is how you could create a None variant of an Option. I specified the type that Some will wrap in angle brackets after the Option. Notice that I don't have a use statement bringing into scope option or its variants Some or None from the standard library. Since Option and its variants are used so much they're already included in the standard prelude, which is the list of items from the standard library that are always brought into scope by default. If you ever use Option with a concrete type, then the compiler will infer the type, which means you can leave the type annotation off of the declaration most of the time. There's a handy helper method called is_some() that returns true if x is the Some variant. There is also an is_none() method that does just the opposite. Option implements the IntoIterator trait, so you can also treat it similar to a vector of 0 or 1 items and put it in a for loop. You ought to read through the methods for Option, because you will end up using them a lot. There are a whole bunch more of them that I won't go over. The other important enum is Result. Result is used whenever something might have a useful result, or might have an error. This turns up especially often in the io module. Here is the definition of the Result enum. First, you'll see that the type wrapped by Ok and the type wrapped by Err are both generic but independent of each other. Second, the #[must_use] annotation makes it a compiler warning to silently drop a result. You have to do something with it. Rust strongly encourages you to look at all possible errors and make a conscious choice what to do with each one. Anytime you deal with i/o failure is a possibility, so Results are used a lot there like I said earlier. Let's see it in action! Here I bring std::fs::File into scope and then try to open a file. This returns a Result because the file might not get opened successfully. Since I dropped the Result without doing anything with it, I get this compiler warning: "unused `std::result::Result` that must be used" The point is: ignoring errors is not a safe thing to do! So let's go choose something to do with our Result. The simplest thing you could choose to do is to unwrap the Result with the unwrap() method. If the Result is an Ok then this gives you the File struct that you wanted. If the Result is an Err then this crashes the program. In some cases crashing the program may be what you want. In any case, you get to choose. Another option is the expect() method. It's exactly the same as unwrap(), except that the string that you pass to expect() is also printed in the crash output, so you can provide yourself a little bit of custom context as to why the crash occurred. Just like Option, there are helper methods like is_ok() and is_err() that return booleans. Here we know that unwrap() will never crash because we've made sure it was an Ok already. Of course, you can always do full pattern matching as well. Here I match on a Result and execute different blocks depending on what I got back. In the next video we'll talk about closures. 25. Exercise G - Collections & Enums: Exercise G: Collections Enums. As is my custom I encourage you to go and take a shot at this exercise yourself. See if you can complete it, have some fun with it, and then come back! All right, now that you've had a chance let's go through it. Someone is shooting arrows at a target. We need to classify the shots. 1a: create an enum called Shot with these variants. Okay. enum Shot. Variants: Bullseye. Next is Hit containing the distance from the center as an f64. Last, a Miss. All right, we have our comma there at the end. You'll need to complete 1b as well before you will be able to run this program successfully, so let's go do 1b. impl Shot, Okay, so we're gonna add some methods or a method to our Shot. Implement this method to convert a Shot into points. Okay, so that means we're going to need to match on my self, which is an enum with the possible values...one is a Shot::Bullseye and if that is the case, what do I return? Okay, well this function is returning an i32...five points. 5. Next, if it is a shot and it is a Hit with some x, then let's go take a look at our x, and if x is smaller than 3.0 then we're at 2. Otherwise we're a 1. So 2 points if we're close enough to the target that it's less than 3 units away. If it's more than 3 units away but still on the target, then it is a 1. But there is a nicer way to use this, I think. I think we can make this a little bit nicer if we used guards. So here if we go "if x is less than 3.0" right there then this entire arm could just be the 2 and then we can duplicate this and say for anything else then we know it's greater than an equal to because we already check the less than. So in that case it's a 1. I think that's a little bit nicer in this case to use a guard up here, and then last we've got Shot::Miss and in that case we are 0 points. That should cover us. Let's go get this try. Whoops! What did I do wrong? 35. Unexpected, huh? Well what did I do. I probably did something wrong here here. Yeah. Look at that. Yeah. Look at that. We got two closing braces for that match. There we go. Let's go give it a try again! Voilà. Okay, main! Simulate shooting a bunch of arrows and gathering their coordinates on the target. So, we've got our arrow_coords which is a vector of coords. We've got a little helper function here that gives us five random coordinates, random shots, who knows where they hit? And then we make an empty vector of shots. So we're going to convert the coordinates into our shots. So for each coordinate in arrow_coords, call print_description so we can see what it was, and then create the correct variant of Shot depending on the value of distance_from_center: less than, between, greater than. Okay, so we're going to convert coords into shots in this step. So for coord an arrow_coords. First we need to call coord.print_description, then we need to append the correct variant of Shot to the shots vector, so let's say let our current shot be...let's match on our distance_from_center. coord.distance_from_center Now this is a floating point and we can totally do matches on that! So x if x is less than 1.0. Then what is our shot going to be. It's going to be a Shot::Bullseye and x if x is smaller than 5.0...we already know it's greater than or equal to one because we just checked it above us. In that case it's going to be Shot and Hit and the value is going to be our x. And then if it's greater than five that's going to be any other condition, so let's just use our wild card here and say in ALL other cases we'll return a Shot::Miss. And then since we're using the value of this match expression we need a semicolon here. Now we've got a shot. It's an enum. It's one of these Shot variants. And what are we going to do to it? We're going to take our shots vector and we're going to push it onto it. Shots push our shot. So let's go out here and go: cargo run. Should have some interesting output now. There we go. Random output, we're still not doing the totals. That's okay. We're putting everything in that vector, but we're not using it yet. If we run it again, we should see our random in action. Random numbers. All right. Let's go back. Number 3: Loop through each shot in the shots. Okay, so for shot in the shots, and add its points to total, so total += shot.points() Remember that method? That method we created up here near the start? points()...and we convert ourself, our enum, into a number that we can then add to our total there, and then our final point total should actually make sense. So let's go give that a try. Clear. Cargo run. All right. So here's our random stuff. So 4.3... that's almost a five, but it's still on, so that should be one point. Two! That's under three, so that's two points. Another two points. So what are we up to, five? And then another one point: that's a six. Another one point. Seven! Final point total is seven! Hey look at that! It works. Different random numbers, different results, but it looks like it's working. Let's try it again. Let's see if we can get a bullseye. Oh! Zero! Missed the target every time! Seven. No bullseyes for us tonight. All right. Oh, there was one! All right then! This was a pretty big exercise, so I didn't make a challenge on this one. So we are all done. 26. Closures: You'll encounter closures when you want to spawn a thread, or when you want to do some functional programming with iterators, and in some other common places in the standard library. So let's learn about closures. A closure is an anonymous function that can borrow or capture some data from the scope it is nested in. The syntax is a parameter list between two pipes without type annotations, followed by a block. This creates an anonymous function called a closure that you can call later. The types of the arguments and the return value are all inferred from how you use the arguments and what you return. So let's assign this closure to a variable called add and then call it with 1 and 2 which will be added together and return 3. Let's go back to our closure again. You don't have to have any parameters. You can just leave the parameter list empty. Technically you can leave the block empty as well, but that's not very interesting. What is really interesting is that a closure will borrow a reference to values in the enclosing scope. Let's look at an example of that. Here we create a string s and then we create a closure that borrows a reference to s, which works because the "println!" macro actually wants a reference anyway. Then we assign the closure to the variable f, and whenever we call f it prints a strawberry. This is great if your closure never outlives the variable it is referencing, but the compiler won't let us send this over to another thread because another thread might live longer than this thread. Lucky for us, closures also support move semantics, so we can force the closure to move any variables it uses into itself and take ownership of them. Now s is owned by the closure and it will live until the closure itself goes out of scope and gets dropped. So we could send this closure over to another thread, or return it as the value of a function, or do whatever we want with it. If you want to do some functional-style programming, closures will be your close friends! Call iter() on a vector to get an iterator and a whole bunch of methods that use closures will be available to you. Here is an example of using map() and a closure to multiply each item in a vector by 3, then filter() and a closure to discard any values that aren't greater than 10, and then fold() with an initial value and a closure to sum the remaining values together. In the next video we will talk about threads. 27. Threads: Rust threading is portable. So this code we are about to look at should work across Mac, Linux, and Windows. It will also work on a whole bunch of other platforms that I'm not going to enumerate. Here is a fully functional, but pretty empty, example. We bring the thread module into scope first, then in our main function we call thread::spawn(). thread::spawn() takes a closure with no arguments. This closure is executed as the main function of the thread. So anything that we would want to do in our thread we would do here. One common practice is to simply call a function from the closure. So we don't have a big, huge, long, inline closure. spawn() returns a join handle. With that handle we can call join(), which will pause the thread that we're on until the thread we're joining has completed and exited. The thread that we spawn could have an error like a panic, or it could return a value successfully back to the thread that joins it. So what we get from the join call is a result that wraps a possible success value returned from the thread, or an error if the thread panicked. Threading is a bit heavyweight. Creating a new thread allocates an operating-system- -dependent amount of RAM for the thread's own stack, often a couple of megabytes. Whenever a CPU switches from running one thread to another, it has to do an expensive context switch. So the more threads you have trying to share a CPU core, the more overhead you will have in context switching. Even so, threads are a fantastic tool when you need to use CPU and memory concurrently, because they can run simultaneously on multiple cores, and actually accomplish more work! However, if you just want to continue doing some work while you are waiting for something like disk i/o or network i/o, then I encourage you to look into async/await, which is a much more efficient approach for concurrently waiting for things. 28. Exercise H - Closures & Threads: Exercise H - Closures G Threads. In this exercise we'll go over closures and threads. If you haven't had a chance to go over this exercise, please do so now. Pause the recording. Come back when you're done. All right. Now that you've had a chance to do the exercise, let's run through it. 1a. We'll use a couple functional methods on iterators: filter and map, specifically, to accomplish our purposes. So 1a: we need to use the filter method with a closure to remove any even number. So let's do that. We call filter on our iterator and we give it a predicate, which is a closure. The argument passed to the closure will be a reference. So we have two choices here. One, we could treat x as a reference or two, we could dereference x inside the argument list. I'll show you both. First, treating X as a reference: dereference x, mod it by two, and check to see if it's equal to zero. The other way is to add an ampersand before x in the parameter list. Then x has already been dereferenced. 1b. Let's use map to square the values. Map also takes a closure. And once again, our parameter is a reference. So I'll go ahead and dereference it. We want to square the numbers and we're done. This should filter out any even numbers, and then square them, and then return the sum. Where to dereference a reference is a matter of style. In this case, I think it makes more sense in the parameter list. Otherwise, it would look like this, which to me isn't really very readable. I'll just undo that. Number 2: Now we are going to spawn a thread and we'll have it call that expensive_sum function that we just altered. I'll uncomment this code and I'll call std::thread::spawn, which takes a closure with no arguments. The closure body needs to be an expression. A block is always an expression, but a single function call is also an expression. So here I can simply do my call. The vector is my_vector, and we're done. Since expensive_sum takes my_vector by value, the closure will automatically become a move closure and take ownership of my_vector, but I'd like to make that explicit. So I'm going to add the move before the closure. Now we're being explicit that this is a move closure. Now we're being explicit that this is a move closure. While our child thread is running, the main thread will print out the letters a, b, c, d, e, f with a 200 millisecond pause in between so that we can see what's happening. Our expensive_sum function starts with a five hundred millisecond pause, so that println ought to come in the midst of the letters being printed out. We're not joining the handle anywhere, so the value returned by the thread will be discarded. Let's give this a try. We'll do cargo run. Looks like it's working. Our child thread launched and the output came in the middle of our main thread's output. 3. Let's get that return value from the thread. 3. Let's get that return value from the thread. We'll take our handle to the thread. We'll join on it, which will cause us to pause until the thread exits. That will return a Result that will be an error if the thread crashed or a value to unwrap if the thread exited successfully. I'll go ahead and assume the thread exited successfully and we'll crash otherwise. Then we'll print out the sum. Let's go give it a try. Cargo run. The child thread's expensive_sum is 20, so it worked. The main thread didn't join the child thread until after the main thread had finished printing out the letters. The main thread didn't join the child thread until after the main thread had finished printing out the letters. Number 4: This exercise is less of an exercise and more of a chance to walk through some code and just play around with it. So I'll uncomment this and walk through it with you. First, we'll create an unbounded channel. Unbounded just means that the channel will buffer as many values as it can until it runs out of memory. Creating a channel always returns a sending side and a receiving side. I've used tx for the sending side, or the transmitter, and I've used rx for the receiving side. Next, we clone the transmitting side. Cloning an end of a channel makes multiple ways to communicate on that end of a channel. For this particular example Cloning an end of a channel makes multiple ways to communicate on that end of a channel. For this particular example I could have used the standard mpsc module's channels, but I chose not to, because in general I always recommend using the external crossbeam crate's channels instead. They have a lot more features and higher performance. Next, we spawn a thread. We store the join handle in handle_a, so this is our thread A. We have a place where we could pause, but we don't. We print out "Thread A: 1". We pause for 200 milliseconds and then print "Thread A: 2", and then we'll exit the thread successfully. While that starts off, the main thread will pause for 100 milliseconds to give thread a a chance to start up and print out "Thread A: 1" Then we launch thread B and store a handle in handle_b. Thread B also pauses for 0 milliseconds. Thread B also pauses for 0 milliseconds. Since the main thread paused for 100 milliseconds, thread A has already printed out "Thread A: 1" and is in the middle of pausing for 200 milliseconds, so we should see "Thread A: 1" and then "Thread B: 1". the middle of pausing for 200 milliseconds, so we should see "Thread A: 1" and then "Thread B: 1". Then thread B also pauses for 200 milliseconds, which should give thread A a chance to print out "Thread A: 2", and then exit, and then our pause here in thread B will finish and we should print out "Thread B: 2". So altogether we should see: "Thread A: 1", "Thread B: 1", "Thread A: 2", "Thread B: 2". Let's go see what the main thread does. The main thread loops through the receiver, printing out any messages received. That's why I referred to the transmission of the strings above as "prints" because the main thread just received them and printed them out. Then for good hygiene, we join the handles. Technically, this isn't necessary because the main thread would never get past the receiving loop until all of the transmitting sides had been closed. Closing all the transmitting sides causes the receiver to close once it's empty. So once the for loop had emptied the receiver, the receiver would have exited, which would cause the for loop to exit, and so we know by this point that the threads have already exited. But for good hygiene we'll join them anyway, just in case the program is refactored. Let's give this a try. Clear. Cargo run. Watch the output. Just what we expected. Thread A: 1, Thread B: 1, Thread A: 2, Thread B: 2. Let's make a change to our timings and see if things adjust as we expect. Let's add a 500 millisecond pause to the start of thread A. Now, I would expect to see Thread B: 1, Thread B: 2, Thread A: 1, Thread A: 2. Let's give it a try. Let's just run that last command again. Just as we expected! Now that thread A has that large pause at the start, thread B outputs first. The challenge is to reverse the direction of the channels so that we are communicating from the main thread to the child threads. I'll let you figure that out. Note that this wouldn't be possible with mpsc channels because they can only have a single receiving end of the channel. That's one of the primary reasons that I recommend crossbeam channels. At the time that I'm recording this That's one of the primary reasons that I recommend crossbeam channels. At the time that I'm recording this I'm not planning on making a video walkthrough for Exercise Z, which is a fun little project that you can take on by yourself as a challenge. 29. Invaders Part 1: Setup Audio: Let's do a project. Let's make a simple Space Invaders game in the terminal. We'll have sounds. We'll have movement. We'll kill some aliens. Along the way we'll use some of the concepts we learned in this course. This project is based off of a live-coding presentation I did for the virtual open source convention for O'Reilly on June 18th, 2020. All right, let's create the project. I will be typing while I talk, so I hope you like the sound of my mechanical keyboard. So let's get started. cargo new invaders We'll just call it invaders for short. I have prepared some sounds, so I'm just going to copy them all in to invaders and then we'll go into invaders. Let's open this up in IntelliJ, which I will be using. I also use Visual Studio (Code). Great tools, both of them! At the moment I find that IntelliJ is a little bit better for presentations, but VS Code is coming up strong, so I wouldn't be surprised if that changed in the future. Let's take a look at Cargo.toml We need to add our dependencies. We're going to need crossterm. We're going to need an audio library. I wrote rusty_audio for the purpose. We need some timers. I put that in rusty_time, a little library. Both of those are under my rusty_engine baby game engine, for educational purposes. Our dependencies will take a bit to compile. So let's go get that going now. So cargo build and cargo build --release just in case we want to compare the two, we'll just get all of our dependencies compiled. While that's compiling, let's go set up our audio. First, in main.rs let's make it return a Result so that we can use question mark ergonomically. So, we don't really care about success. But for error, that's going to be--we're going to box up a dynamic error trait object. And got to import error, But for error, that's going to be--we're going to box up a dynamic error trait object. And got to import error, thank you, IDE. And then on this line, we can start setting up our audio. So let's make a mutable audio, which is going to be our audio struct. Then "new()", import that, that's from rusty_audio. Then we need to add all of our audio sources to our audio manager here. So the first argument is going to be the name. Then the path, all of our files end with wav. So I'll go ahead and add that, we've got, let's see, one, two, three, four, five, six. Can see them over there on the side. Four, five, six. So, we're just going to add each of these. So, first we've got "explode". Then we've got "lose" for when we lose. And we've got "move" for when the bad guys move and we've got "pew", you know, "pew, pew" the laser that will be firing. And then we've got a "startup" sound and a "win" sound. All right. Now we need to play the sound. We're going to play "startup" because we're starting up. OK, if we stopped there OK, if we stopped there we wouldn't be able to hear this, because the audio system plays the audio in a separate thread in parallel. So if we just left it like this, we'd run off the end of main, and that would cause all of the threads to shut down. And so we wouldn't hear anything. So let's have this cleanup section where we say audio.wait(), So let's have this cleanup section where we say audio.wait(), That will block until all the audio is done playing, and then we need to return our Ok. and then we need to return our Ok. Now we should be good to go, let's give this a try. cargo run [musically] "bum-budda-bum!" There we go. That's our startup sound. Yes, all of the sounds are me talking into a microphone. If someone would like to contribute some sounds, that would be fantastic. I'm releasing this open source on GitHub under the MIT and Apache 2.0 licenses, your choice. So if you want to come and contribute to this project after the videos here, I'm just gonna let it keep going. So if you want to come and contribute to this project after the videos here, I'm just gonna let it keep going. If people want to extend it, that's fine! In the next section, we will setup our rendering. 30. Invaders Part 2: Rendering & Multithreading: Invaders Part 2: Rendering and Multithreading. Now that we've got our audio working, let's initialize the terminal. The first thing we need to do is get access to stdout. Next, let's enable raw mode so we can get keyboard input as it occurs. We're going to use our question mark operator here, which will essentially crash if we have an error. "terminal" is from crossterm. Then, let's enter our alternate screen. We're going to use an extension that cross term provides on stdout called "execute" to immediately execute something. We're going to enter the alternate screen. If you've ever used vim or emacs in the terminal, you'll notice that when you launch it, you get the vim or emacs screen, but when you exit, you're right back to where you were. That's because the editors are using the alternate screen. There's two screens available in the terminal. And that's what we're going to do. We're going to enter our alternate screen, play our game, and then we'll exit back to where we were. Finally, we need to hide the cursor. We don't want to see it bouncing around as we render our screen. All right. So now that we've set everything up, we need to also do the reverse at the end. Otherwise we'll remain in this mode when the program ends. So let's go down to the cleanup section and do everything in reverse. First, let's show our cursor. Then let's leave the alternate screen. And finally, let's disable raw mode. And I need to remember to put a question mark over here as well, or we'll get a warning. All right. Now, let's go try this out. [music] "Bum budda bum!" Okay, there we go. The screen went blank as we entered the alternate screen. Nothing's there yet. We got our sound and then when our sound finished playing, we backed everything out. Our cursor was also hidden while we were on the alternate screen. So it looks like everything's working. Next, let's set up our main game loop. So between our setup and our cleanup, let's add our game loop. We're going to name the game loop "gameloop" That's so we can exit it from anywhere in the loop by name. And let's do some input handling. So this is a little bit verbose, but we're going to poll for input events. So the polling function here takes a duration. So we're going to use the default duration, which is zero. We're not going to wait. We're going to return immediately if there's nothing to act upon. At this point, we know we have some type of event, but we only care about key events. So let's look at those specifically. All right. Now we know we've got a key event, so let's match on that key event or more specifically, the key event's Now we know we've got a key event, so let's match on that key event or more specifically, the key event's key code. All right. Now we're looking at specific key codes. So what do we care about? Well, let's make it so we can exit if we press escape or "q". If we get either of those, then, okay "q". If we get either of those, then, okay we've lost the game because we exited early. So let's play, audio.play("lose"), and break out of the game loop. Great. Now if any other key is pressed, we're just going to ignore it. And that is our first version of our game loop. Just looking for a key to make us quit. Otherwise, we will loop infinitely. Let's go give it a try. [musical] "Bum budda bum!" All right. We're in our alternate screen and we're hanging out here because the game loop is waiting for us to press a key. If I press "j" nothing happens. If I press "q" [sound] "you lose" There we go. Now, let's go make some frame logic. I think we're ready to start making a nice library and start organizing our code a little bit better. So let's come out here to the files and I'm going to make a lib.rs, which will be the root of our library. All right. So here's lib.rs. Let's make a "pub mod frame". We're going to have some frame logic for every frame that we want to render. Let's also make a couple constants. Let's make "const NUM_ROWS" for the number of rows that we're going to have in our playing field. Set that to 20. And "pub const NUM_COLS" for the number of columns that we're going to have. Both of these are usizes, and I'll set this one to 40, and now we can go over and create our frame.rs. So back over to our files, make a new Rust file called frame.rs. This is going to be our frame module. And I'm going to go ahead and use a type alias because our frame is going to be a vector of vectors of borrowed, static string slices, which is not only a mouthful to say, but it is a mouthful to type. So we'll make this type alias, just call it Frame, vector of vectors of borrowed, static, string slices. You can see why we might want an alias for that. All right, let's make a public module function. So pub fn, and we'll call it "new_frame". This will just generate a new frame for us. And here's where we benefit from our type alias, we can just return frame. This is going to be a vector of vectors. So we need to make our outer vector first. So let mutable, we'll call it columns, Because we're going to do column-major, so we index in by column first (x) and then row (y). That's going to be a vector. We're gonna do a little bit of optimization here. I'm going to start it out with the capacity, since I know what it is and we're going to be generating one of these, every frame--performance might be important. And then let's loop through every number. I don't need the actual number, but the number of times that we've got columns. And then for each one of these, we need to generate a column, and then at the end, we will return a [vector] of columns. So in here, we'll go ahead and say our individual column vector. I'll just name that "col". It is also a vector. We also know the capacity. This time it's going to be NUM_ROWS. And now we just need to loop through and add a single row character for each row. So for the number of times in 0 to NUM_ROWS, col.push. We're going to do a blank space. So this is going to be our blank frame. We will create a new frame with our logic every time around the game loop. And then we need to remember to take "cols" and push on the "col". And there we go. That should be the entire thing for adding our new frames. Now, everything that we want to see is going to need to be able to draw itself into the frame. So let's go ahead and add a trait. So I'm gonna call this trait "Drawable", so "trait Drawable", and to be Drawable, that means you need to implement this method, which I'll call "draw". need to implement this method, which I'll call "draw". Where you take an immutable reference to yourself and a mutable reference to a frame, and then you draw yourself into the frame. So that's the definition of our trait. So that's all we need for our frame logic. Let's go ahead back to our lib.rs and let's add another module for rendering our frame, our conceptual frame, out to the actual terminal. So we'll separate, sort of, our model from our view, in a sense. So let's go "pub mod render" for a render module. Go back over to our files. Add a new Rust file. Call it a render.rs. Let's make a "pub fn render". What do we need to render to? Well, we need a stdout. So let's take standard out as a mutable reference and then we'll also do a little bit of optimization in our rendering. We will only render what changed. In order to do that, we need a last frame, which will be a reference to a Frame, and a current frame, which will also be a reference to a Frame. Now that's great for most of our frames, but at least once we're going to need to draw everything. So let's also add a "force" boolean option to force rendering the entire frame. All right. So let's handle the forcing first. So if we're forcing rendering everything, then there's a few things we want to do. We want to clear the screen and I'm going to go ahead and clear it to blue. So that when we draw our playing field in black, it will be offset and we can see the border of the playing field. So we're going to take advantage of crossterm's, queuing [trait] to queue up a bunch of commands to the terminal and then flush them all at once. So "stdout.queue", we're going to set the background color to the color blue. And then we will crash if there's an error. We should only see a crash if our program isn't connected to a terminal, so I'm not concerned about hitting this. Next, we need to actually do the clear operation, so "Clear" and what is our clear type? Clear type is "All", we're going to clear all of the screen. Again, we'll crash if there's an error. And then lastly, we will set our background color to black. So go ahead and change that to black so that the playing field that we draw will have a black background. Next, we need to iterate through our entire frame and draw whatever's changed. So for every x index of column vectors in our current frame, which we'll iterate through immutably and enumerate to get that x index, and then we'll do the exact same thing for y and the actual string character that we're looking at in our column. Iterate through that immutably, enumerate that to get the y. Great. So now we've got the x and y index and we've got the actual character at our current frame's location. So let's compare that. So if--now "s" is going to be a reference of a borrowed string slice, so it's double referenced. So I'm going to dereference it one level so I can compare that with the last frame's character in x and y. So I'm going to dereference it one level so I can compare that with the last frame's character in x and y. So if the character changed or if we're forcing rendering, either way, then we will queue up a command to move to the correct location. So here's our "MoveTo", and then our x needs to be a u16 for this call and our y as well. And then, once again, we'll crash if there's an error. And then we will print without a line, without flushing, a single character at this location, which is our dereferenced "s" again. We've done a whole lot of queuing, so we need to remember at the very end to come and take standard out and flush that all. in one, big update to the screen. So here's our rendering. This should take our frame logic and then iterate through things and render out our game to the terminal itself. This should take our frame logic and then iterate through things and render out our game to the terminal itself. So now we've got our logic. We've got our rendering. We need to go start hooking these things up. So let's go back to main and we will first add a render loop in a thread. So we're gonna do some--example of multithreading here. So after we set up our terminal, we'll go ahead and say let's make a render loop in a separate thread. I measured this. It actually does provide a little bit of speed up. We probably don't need the speed up, but, you know, you may actually have a need for this in the real world. So let's demonstrate it. Let's set up a channel to communicate with our threads. So let's let a render transceiver in a render receiver side of the channels. I'm going to use mpsc channels just because they're built in to the standard library and they're super simple to use in a demo like this. Now, for a real project, I encourage you to go use crossbeam channels instead. They're higher performance and they have more features. But I'm just going to stick to the dead simple mpsc channel for this example. Let's make a thread now. So I want to catch the thread handle and this variable, so our render handle. That's going to be std::thread::spawn() So I want to catch the thread handle and this variable, so our render handle. That's going to be std::thread::spawn() And that takes a closure and we're going to use a move closure so that we capture our end of the channel that we're using. And then the logic right here is going to be the logic of our thread. It's not going to be a lot. So I'm just gonna do it right inline rather than making a new function or something. So first thing we need is we need a variable to hold our last frame because we're gonna hold on to that and we'll just initialize that to a new empty frame. That's a great starting point. Next, we're in a separate thread. We need to render to standard out. So we need our own access to standard out. So I'll make another "mut stdout", "io::stdout()". And then we're ready to actually render the entire screen once. Do that blanking, full, forced render. So that's a render module's render function. Here we give it our mutable reference to stdout are immutable reference to last_frame. We don't have a curr_frame so I'm going to give it last_frame again. And that's okay because we're going to force rendering everything. Okay. So now we've setup the screen once. Now we can do our incremental updates. So here's our loop. We need to get our current frame. So that's going to be render receiver, receive. This is where our move closure comes in because render_rx is up there on line 29 and we're moving it into this thread. Now that returns a Result. It either returns a current frame or if the channel's been closed or returns an error. So let's match on that Result. And if it is Ok(), if it's actually a Frame, then we're just going to return the Frame. And if it's an Err(), then we'll just break out of our render loop, which will shut down our child thread. And if it's an Err(), then we'll just break out of our render loop, which will shut down our child thread. Okay. So this is going to return a current frame, but we're not doing anything with that. So let's go and make a variable here. So let the current frame be the result of that match expression, which means we need to add a semicolon here because we're using this in a statement. So now we're ready to actually render our frame. So render, render. Give it our mutable stdout. Give it our reference to our last frame, a reference to our current frame that we now have and false. We are not forcing rendering everything. Only one thing left: we need to do some housekeeping. So last frame is now current frame. That sets us up for the next time around the loop. We've got last frame all setup correct. So now we've got a thread that is ready to receive frames and render them. It's all hooked up, but it's not receiving anything. So let's go start supplying it with new frames. If we come down to our game loop here, first thing we need to do is at the top. We need a per-frame initialization section. So here's our per-frame init. We'll let our current frame be a new frame. Now we want to handle input next so that we can make any changes to our logic before we render. So let's come down to after our input. And now we can make a draw and render section. So first thing we want to do is send that frame. So render, transceiver side. Send our current frame. We don't need it for anything else. So we're actually moving it to a different thread. Now that returns a Result. But we expect this will fail the first few times because this game loop is going to get going before that child thread is set up and starting receiving. So there's not going to be a receiving end of the channel available for a little while. So instead of crashing on error here, which would instantly crash, we're going to ignore the error entirely. So instead of crashing on error here, which would instantly crash, we're going to ignore the error entirely. So let whatever result just be silently ignored. Okay. And I also know, through testing, that this game loop is much faster than our render loop. So I don't want to get continuously behind on rendering by rendering hundreds of thousands or millions of frames per second. So I'm going to add an artificial sleep. So just thread sleep duration and I really don't need to sleep much. I'm just gonna add this single millisecond of sleep and that will be enough. That limits us to no more than generating a thousand frames per second, which we can actually keep up with. That limits us to no more than generating a thousand frames per second, which we can actually keep up with. Since we're only updating what changed. Now that we've got this setup and sending, we still need one final bit of cleanup. We need to come down here and actually join that thread. Now, we could probably ignore that and, you know, not make sure that our thread cleans up and just kill it when we exit. But it's much better hygiene to clean it up. So I'm going to explicitly drop our render_tx. Now, newer versions of Rust don't require that. I tested that. Non-lexical lifetimes, I think, made it possible for Rust to detect that it was no longer used in a smaller scope and drop it earlier. But older versions of Rust won't know that. And so we'll just do this here. We'll be a little bit more compatible and explicit. And then once the transmitting side of the channel has been closed by dropping it, the receiving channel will error and it will exit out of that loop that we made. So we know it's safe to now call render_handle.join() and it won't wait forever because we know that that render loop is going to shut down. So now we've done this nice clean up of our thread. We shut down our sending channel. That will trigger an exit of the thread and then we wait until the thread actually joins. That's it. Let's go give it a try. This is our largest section between trying things out. So in the future, we'll be able to try things more frequently. Cargo run. [music] "Bum budda bum!" And there we go, we can see we cleared the entire screen, the blue. And then we came through and we rendered our playing field, which is black. And now we can move on. So in the next section, we'll look at making our player logic and rendering him to the screen. 31. Invaders Part 3: The Player: Invaders Part 3: The Player. Yes, we are finally to the place where we're going to implement the player. The first thing that we're going to do is add the player module. So "pub mod player". Next, let's go over and add our player.rs file. New Rust file. player.rs Now let's create a struct for our player, so "pub struct Player". What do we care about most? Well, we care about x, so its position horizontally. That is usize. "y" also a usize. And that's really about it at this point. So let's implement some things. What logic do we need to implement for player? Well, we need some way to make a player. So "pub fn new". Going to make our canonical constructor here. We'll return our Self, which is a Player. The x we'll set to NUM_COLS divided by two. That'll put us roughly in the middle, and then our y we'll set to NUM_ROWS minus one. So the "y" starts at zero at the top of the screen and then as y increases we go down the screen. So "NUM_ROWS - 1" is going to be our last playable row. Don't do the off-by-one thing! Next let's make some way to move left. So "pub fn move_left", mutable self because we need to change ourself. Let's do some bounds-checking. So if self.x is greater than zero then "self.x -= 1". That's left. Let's move right now. So "pub fn move_right" So "pub fn move_right" Just the same thing, only in the opposite direction. Bounds-checking again. If self.x is smaller than "NUM_COLS - 1" Here's our off-by-one thing again. "self.x += 1" Okay, great. We can move left and we can move right. What else do we need to do? Well, I think that's about it for the logic. But we do need some way to draw our player into the frame so he'll be rendered. So let's go down and implement Drawable for the Player and then we'll implement that draw method. So here we've got access to a mutable frame. Let's use it. "frame", index in by x and by y, and set it to the character that represents our player. So we'll set that as an "A". Now, this is not a capital A! This is a ship! This is a spaceship! If you're looking top down, the spaceship's pointing northwards. It's got that cross bar. So obviously that front part is, you know, where the pilot is. So, yeah, obviously a spaceship. So now we've got Drawable implemented for Player. So now we've got all the logic we need to display a player and move him left and right. So let's go hook it up. So let's go to main.rs. We'll go down to our game loop here. Right before our game loop, we'll get a mutable player. That is "Player::new()" That is "Player::new()" And then, let's go to the input section and handle moving left and right. So, keys here. Let's do KeyCode::Left. So this is the left arrow: that moves the player left. And if the key code is, bet you can guess where I'm going with this, right, player.move_right() player.move_right() Great. So we've got left and right hooked up to input. We make the player, we update him based on input. Now let's actually draw him into the frame. So let's come down to our "Draw & Render" section. And right before we render the frame, we'll draw him into the frame. So the player.draw, give him that mutable current frame. Okay. Need to import the Drawable trait, so that that works. What are we missing? Ah! curr_frame...cannot borrow the immutable local variable curr_frame as mutable. So let's go back up here and make that curr_frame mutable. Otherwise, we'll never be able to draw anything into it. Now, I think we've got everything all hooked up. Let's go give it a try! cargo run cargo run [musical] "Bum budda bum!" Okay, here we are. We see the A in the middle. Try left. Hey! Doesn't go off the side. Try right... Doesn't go off that side either. Great! We have the beginnings of an actual game, we have a player we can move around here. [sound] "You lose." Now that we've got a player, in the next section we'll see about shooting. 32. Invaders Part 4: Shooting: Invaders Part 4: Shooting. Now that we've got our player working, let's make it so that he can shoot a laser bolt upwards. So let's go to lib.rs and add "pub mod shot". Come over here and make a new Rust file. shot.rs Great. So we've got a shot module. Let's make a struct called Shot. What do we care about data-wise? Well, it needs an "x" and a "y". Let's also keep track of whether the shot is currently exploding and we'll need an internal timer to keep track of our movement. Let's implement our shot logic. As always, we need some way to construct a shot. So let's make a new. And we'll need an X and a Y. And we'll return our Self. So our Self. Of course, our x will be x and our y will be y. We can use shortcuts on those. "exploding" will be false because we've just started and timer will be our timer from rusty_time and we'll make that a fifty millisecond timer. So our laser will move upwards one cell every 50 milliseconds. Pretty fast little laser. Pretty fast little laser. Next we'll need some way to update that timer. So let's make a "pub fn update". It will need mutable access to itself. And let's pass it in a delta duration from standard time. All right. What we need to do here is take our self.timer and update it with a delta. That will make the timer actually start counting down by "delta" amount. And then let's check. So if the timer is ready and we're not exploding, so "!self exploding". Now we can actually move. So the timer is ready to move and we're not exploding because we don't want to keep moving after we explode. So let's do a little bit of bounds-checking here. If self.y is bigger than zero. So if we haven't yet reached the top of the screen, then we can move upwards. We don't want to move off the top because then we'll go out of bounds. All right. Great. So now we've actually moved, if that's the case. And either way, we need to reset the timer. So self.timer.reset(). Now we have the logic to actually move our laser shot upwards. Now let's make some way to actually explode. "pub fn explode" We need mutable access to our self again so we can change that exploding variable, so self.exploding field is true. And then let's take that timer and 50 milliseconds isn't really long enough to see an explosion very well, and 50 milliseconds isn't really long enough to see an explosion very well, so I'm going to quintuple that. Let's take our self.timer and set it to a new timer. And this time, it's going to be 250 milliseconds long. Great. That'll give us just enough time to sort of see the explosion before it disappears. Next, let's do a function to tell when we are dead. So someone else is going to need to clean this up. In fact, it's going to be our player that's going to be managing our shots. It needs to know, It needs to know, Is this shot dead? Does it need to be cleaned up? So let's make a "pub fn dead" So let's make a "pub fn dead" In this case, we only need to look at ourselves, so we'll get immutable access. Return true or false? Are we dead? Now, this is just a condition. So we can just have a boolean tail expression here. So, self.exploding and self.timer.ready means that we've exploded and the time has run out. So that's one case. Or if self.y is zero, then we reached the top of the screen and we need to be cleaned up. So bounds-checking again. Bounds, bounds everywhere. Great. So now we've got logic for a shot, but we can't see it yet. So it's come down here and implement Drawable for our Shot. All right. So here's our draw command. So here's our draw command. We have a frame that we can index into by self.x and self.y This should look a little familiar. This time, though, instead of setting it to a single character, we're going to say if we're exploding, then we obviously want to be the explosion character, which is an asterisk, of course. Otherwise we want to be a laser, which is a pipe in this case. And that's it for our shot logic. So now let's go over to player and add shot handling to the player. First thing we're going to do is add a shots vector of shot to our player struct here. I'm going to depart from the classic "You can only have one shot" here, and I'm going to say we can have multiple shots. So I'm going to use a vector, because I think it's more fun. Next we come down to new(). We need to add that shots vector. So that is a Vec::new() and then we need to add the logic to actually shoot. So let's come down and add a "pub fn shoot". We're going to need mutable access to our self. We're going to return a boolean indicating whether or not we actually successfully shot, because if the max number of shots are already on the screen, we might not actually successfully shoot. But if we do successfully shoot, we want to play the laser sound, the "pew pew". So if our self.shots length is smaller than--I'm gonna set it to two. I'm not going crazy here. You can have two shots at a time. If you want, you can set that a little higher. Then our self.shots needs to get pushed onto it a Shot::new() Where? Well, it's at our own x location because it's directly above us, and since it's directly above us, that means self.y minus one, because the y axis is inverted. And then true we shot, otherwise false we didn't shoot. So there we go. There's our shoot logic. Next, let's make some way to pump those timers on our shots. So we need a "pub fn update", mutable access to our self, and we need that delta duration. And then we just need to go through every shot in our shots. We'll iterate through that mutably so we can change them, and just call shot.update with our delta. And then, of course, we need to clean up after ourselves. A shot might have reached the top, or it might have finished exploding. So we will take our shots and retain only the shot where it is not dead. Little bit of functional programming there. We're using a closure here. That's what retain takes, and it'll apply that closure to every single item in the vector. That's what retain takes, and it'll apply that closure to every single item in the vector, and if it returns true, then it keeps it. If it returns false, then it removes it from the vector. So now let's go down to our draw call here and draw our shots as well. So that's fairly straightforward as well. So simply go: for shot in self shots. Iterate through this immutably because we don't need to change anything. We can just call shot.draw the frame. Frame is already a mutable reference to a frame here, so we don't have to change anything. So we've implemented our shot logic. We've hooked it up to the player. So now let's go back to main and make sure that things are flowing through. So go back to main, go to our input handling. There's got to be some way to actually cause that shot to happen. So let's go here and say if the key code is the character "space bar" or the key code is the "enter" key, Then-- All right. So we want to do player.shoot, but that returns a boolean. So if player.shoot, then we can play our audio: play our "pew" sound. Now we have most of the logic hooked up. But what about those timers? We don't have any timers getting pumped here. So let's go add a section here for our--updating our timers. Call that "// Updates" and we'll just do player.update with a delta and that will flow down to the shots. But what is delta? Do we have a delta? No, we haven't made a delta yet. So let's go up and add this. I like using instant-based deltas. So if we go right here after our player and say let mutable instant, an Instant::now(). Instant is from standard time. Then we can come into our per-frame initialization and before we do anything else, let's just say let our delta for this frame be our instance elapsed time since it began its lifetime. Now, since we did that, we need to then, instantly, come and update our instant to the next "now". Now, the next time around the loop, we will have measured exactly the time that it took to go around the loop. Now, the next time around the loop, we will have measured exactly the time that it took to go around the loop. So now we have the delta. So now this player.update(delta) should work. And I think we're all ready to give this a try! So, save that. "cargo run". [musically] "Bum budda bum!" Okay, we can still move, and if I hit space ("pew") or enter ("pew") or hold them down. [sound] "pew, pew. pew, pew. pew, pew" Great. In the next section we'll make the logic for our invaders. 33. Invaders Part 5: Invaders: Invaders Part 5: In which we create the actual invaders that are project is named after. Let's make our invaders module. So that's "pub mod invaders". Come over here. New rust file "invaders.rs" New rust file "invaders.rs" New rust file "invaders.rs" You can probably guess how we're going to start! "pub struct..." What are we going to make? Let's make a single Invader. Just one invader. He's got an "x", which is a usize, a "y", also a usize, and that's it. So that's just going to be our invader. He's got a location and that's all we really care about him right now. Because more important than an individual invader is our "pub struct Invaders". This is going to be our invader manager or our army of invaders. So let's make a "pub army", which is a Vec of Invader, because they all do the same thing, and they're going to have a move timer to move the whole army around. We're gonna have a direction--that's going to be left or right, primarily, although they do move downwards when they hit the side. All right. "timer" is from rusty_time. Let's go implement our logic. So what logic do we need to implement for our invaders? Well, obviously, they need a "new", so "pub fn new" returns a Self. And we'll make a mutable army because we're going to push some things onto it. That's a Vector::new() of invaders. And then for every "x" in all the possible x's on our playfield, and for every "y" in every row on the playfield, let's make sure to import those. Okay. And you know what? This is really annoying that it's got this warning. So let's go add our little return here at the end and come back and finish the logic. So Self is what it's going to return. What's it going to return for the army? Well, that's going to be the army. For the move timer let's give it a Timer::from_millis. Let's set that at 2 seconds. That's 2000 milliseconds. And the direction we'll start in is right. So positive is to the right. So that's what we're returning. Let's fill up that army so it's not empty. So just do a little bit of math here. I sort of figured this out. Makes a nice grid. If we say, okay well, if--we don't want x all the way at the left side so it has to be in a little bit. So it got to be greater than one. And let's say the same thing on the other side. x needs to be smaller than NUM_COLS - 2 So it needs to be the same amount in from the other side, but it's 2 instead of 1 because of our off-by-one thing. Okay. And then similarly we don't want it all the way at the top. So y needs to be bigger than zero. Remember as y increases we go down the screen. Okay. We also want to stop somewhere midway in the screen. So I'm just gonna hard code this. You might wanna make something more intelligent or based off of NUM_ROWS, but I'm just gonna say y is smaller than 9. Seemed to work okay. I'm just gonna say y is smaller than 9. Seemed to work okay. And then I'm gonna say: Okay, it needs to be on an even x. I don't want it on every single cell. That's a little crazy. So we'll say x mod 2 is equal to 0. So it's only on evens and same thing with y. So y mod 2 is equal to 0. So that should give us a decently-sparse grid of invaders. So let's actually push an invader, so army.push our invader. Didn't bother making a "new" thing for them because this is the only place where access again. So what the heck. Just do the full verbose syntax and there we have it. Let's scroll a little bit. There is our complete logic for our new. We're going to do quite a bit of army logic now, so I'm gonna go ahead and make this a little bit wider. Next, we need to update so we've got some timer and army movement here and it's going to get a little long. Next, we need to update so we've got some timer and army movement here and it's going to get a little long. So bear with me here. It's probably our longest method of the project. So "pub fn update" it's going to take its mutable access to itself and a delta duration. Got to update those times, and we're going to return whether or not the entire army moved, because if they did, we're going to play that move sound. So self.move_timer needs to be updated. Let's update it with the delta first so we don't forget that. Now we can do our move logic. Are we going to move at all? If we're not, then let's just return false. If we are, though, let's go if move timer ready if move timer ready then we're gonna do a whole bunch of stuff and at the end we'll return true, then we're gonna do a whole bunch of stuff and at the end we'll return true, you actually moved. Now we can do our logic right here in the middle. First thing we want to do, since the move timer was ready, we need to reset it. So self.move_timer.reset() So self.move_timer.reset() Great. That's out of the way. Next, we need to determine: is it time to move downwards? So let's make a "mut downwards" and default that to false. Now let's try to tell if we need to move downwards instead of left or right. So if I'm currently moving left, which is negative, then let's get our minimum x out of all the army. So here's another good place for some functional programming. So we'll do our self.army, which is a vector, iter() to get an immutable iterator and then let's map every invader to his x value and then let's take the minimum x value and then that's fallible, because if the vector was empty, for example, there might not be in a minimum. So we'll do the unwrap_or give us a zero if you can't find anything. So now we've got a minimum X or it'll be zero if it didn't find anything. So if min_x is zero, then you're all the way on the left side of the screen. So actually, you need to move down instead of left. But next time you'll move right, so let's change direction over to right, and set downwards to true. And then later on in this method, we'll know that we need to move downwards instead of right. Okay, now otherwise, we're trying to move right. otherwise, we're trying to move right. But we might have hit the right side. So we're gonna do the exact same thing, only for the right side of stuff. So this time we're gonna look for a max_x. So are gonna do the same thing only we're going to do max. So self.army.iter(), map our invader to his x value again. Take the max and then unwrap or we'll say zero because it really doesn't matter what the value is that we give back. So if max_x is NUM_COLS - 1, that means we reached our right side, and so our direction needs to flip. So negative one again and downwards is true. Okay. So at this point we know that we're moving. And we know whether or not we're moving downwards or to the side. So let's go handle that actual movement. Scroll down here a little bit. Right here. If downwards, then we're moving downwards. Else we're moving to this side. So if we're moving downwards, then let's get a new duration, because whenever we move downwards, we're going to increase their speed. So the movement timer is actually going to go to a smaller value. So that's what I'm doing here. Setting the movement timer to a smaller value. Let's get the new value. It's going to be the max of our current move timer. Duration as milliseconds, minus 250. That's really our default. That's what I'm looking for. But if it goes below 250 I'm going to bring it back up to 250, because I don't want to get them too fast! Need to import that max from standard compare. Now we can go self.move_timer is a timer.from_millis that we just determined from new duration. Only we need to cast that to a u64. Only we need to cast that to a u64. And now we can finally loop through every invader. So for invader in self army, iterate through it mutably and say invader.y += 1 to move it downwards. The moving was actually the easiest part of all. Else move left or right. So for invader in self army, also iter_mut, invader.x is equal to, okay here we need to go invader.x as an i32 so that it can go negative. Plus self.direction--I should say, self.direction is an i32 and so to add them together they need to be the same type. So they go self.direction, and then cast that all back to an unsigned usize. So that'll change our invader's x, we return true: play a sound or false: don't play sound. So now we have our logic for invaders. What about the ability to see them? So let's go and do impl Drawable for Invaders. Import drawable. for invader, we need to draw each individual invader. So for invader in self.army.iter(), go through them immutably because we don't need to change them. Frame, invader x invader y, equals--we are going to do something special here. You know how in space invaders if you've ever seen that classic game, the aliens sort of wave their tentacles or arms or whatever it is or blink their eyes? We're going to do something similar! Half of the time we're going to do one character and half of the time we're going to do another character. So while they're in place, it will look like they're doing some kind of arm waving or something. So if we need to take that timer and calculate which half we're in, so we're gonna take self.move_timer So if we need to take that timer and calculate which half we're in, so we're gonna take self.move_timer The time left as f32. Divide that by the move timer's duration as seconds, f32. Divide that by the move timer's duration as seconds, f32. Divide that by the move timer's duration as seconds, f32. Let's get this on separate lines so we can see this better. Okay, so if we take time left, divide it by the total duration of the timer and then compare that to 0.5 then we return one character. So let's do an "x" for one state. Else we'll return another character...and a semicolon there. And if I did that all right, then we're ready to hook this up in main. So let's go do that. main.rs First thing we need is some Invaders. So let's come to the top of the game loop--above the game loop let mut invaders equal Invaders::new() let mut invaders equal Invaders::new() Next, invaders is not controlled by input. So nothing to do there. It is definitely updated by timers, though. It's all driven by timers. So, remember, our invaders.update() returns whether or not we should play a move sound. So if invaders.update(delta), then audio play move sound. So now we've got the logic, but we still won't see them unless we draw them into our frame. So here I could do this. I could go invaders.draw, mutable current frame, and that would work. But I want to demonstrate using generics with a trait. So I will do something harder but more generic. Let's make a drawables, which is going to be a vector of anything that implements the Drawable trait and make that be a vector of a reference to the player and a reference to invaders, and then I can say for every drawable thing in the drawables vector, which I going to consume, drawable.draw(). The only thing I know about drawable is that it implements the draw [method]. So this is literally the only method we can call on it. And what does it need? It needs a mutable current frame. And then these two lines are no longer needed. So, yes, this is a little bit more complicated. Just trying to demonstrate. This becomes a lot more powerful if you use it with a function or something like that. And we get the payoff. If we added more and more Drawable things. All we would have to do is come to the end of this vector and add them to the vector and we wouldn't have to change any other logic. All right, let's go run it! All right, let's go run it! cargo run [musical] "Bum budda bum!" [musical] "Bum budda bum!" [sounds] chook, chook Do they go down? So they went right, they're going down. You can see them animating halfway through their cycle. And if they get to the left side--come on! Yes! They went downwards. Okay. We can still move. We can still shoot. The laser doesn't interact yet. But that's what we're going to fix in the next section. [sound] "You lose" 34. Invaders Part 6: Winning & Losing: Invaders Part 6: Winning & Losing This is going to be our last section of the invaders project. We'll make it so we can win, we can lose, by killing the aliens or they reach the bottom of the screen. There's a lot more that we could do. You know, little aliens going across the top. Keeping track of scores. Adding those little obstacles that you can hide behind and shoot through. In fact, if you want to come and try contributing that either on your own, in your own little fork, or make a pull request and try to contribute it back to the open source project. That's fine. I'm totally fine with us continuing to modify this on GitHub. So let's get to it. Let's go back to our invader's and make it so that we can detect whether we've won by killing them all, lost if they reach the bottom of the screen, and definitely make it so that we can kill them at all. lost if they reach the bottom of the screen, and definitely make it so that we can kill them at all. So we need to come to their logic here and add some new methods. So first, "pub fn all_kills()" will tell us whether or not we have killed all of the invading army. That's going to return a boolean. This one's really simple. It's just self.army.is_empty() It's just self.army.is_empty() Great. Just a tail expression. Next, let's do "pub fn reached_bottom()" So here's our lose condition. Also, immutable access to our self. Return a boolean, because it's just a yes/no. And also a tail expression, although a little bit more complicated. Let's take our self.army Let's iterate through it immutably. Map all of our invaders to their y position. So we're looking at how far down they've come. We're taking the max, because as y increases they go down the screen. That's fallible. So we need to do unwrap, or we'll assume they're at the top of the screen. (Really, they're probably all dead.) And then if that is greater than or equal to NUM_ROMS - 1, then we've reached the bottom. And finally, we need some way to actually kill an invader. So "pub fn kill_invader_at()". So here's an attempt at killing an invader. So, mutable access to our self in case we do kill him. We need the x, the y, you can probably guess that these x's and y's are going to come from shots. And this is going to return boolean so that we know: Did we actually kill an invader? Should we play an explode sound? Okay. So what we're going to do is we're going to say "if let Some(idx)" be our self.army--I want to do this vertical so we keep it on screen--iter, position So position takes a closure, again, so our invader--for each invader--we'll apply Well, ff the invader x is x and the invader y is y, then it will give us our index so that we can do self.army.remove(idx) self.army.remove(idx) There is an actual method on an iterator that can do this all in one method call. One sort of functional method call, instead of checking the index and, if we find it, then removing it. One sort of functional method call, instead of checking the index and, if we find it, then removing it. But at the time that I'm recording this, it's unstable. So it's only a nightly Rust, not in stable Rust. So I'm sticking to the older stuff here. And then the true or false is our return. Whether or not we actually removed someone from the army. Whether we actually killed an invader, so we can play that sound. Now, let's go over to the player module and hook this up. So Player is going to need a "pub fn detect_hits" It needs mutable access to itself and it also needs a mutable reference to an Invaders. So it can try to call that method we just created. Make sure to import that. That's going to return a boolean because we need to pass through that explosion sound if that happens. So did we hit something? And we're going to say, well, first off, no, we don't think so. And we'll return that at the end. So there's our answer in the middle. We'll say, well, for every shot that we've got, might be none, might be up to two. If you'd used a different number, it might be more--need to iterate through them mutably. And then if the shot is not exploding because an exploding shot doesn't get to hit another alien. That'd be silly. So if invaders.kill_invader_at. Here's our attempt. shot.x shot.y Then, yes, we hit something. Then, yes, we hit something, so set that to true and also we need to tell our shot: you are now exploding. So now we'll display that asterisk character, which is our grand explosion character. So now that we've wired this up with player, let's go back to main and wire it up in the main loop. So back to main. We've already got invaders. We've already got player. So here in our updates, let's add a: if the player--ask it to detect hits with all of its shots, it needs a mutable invader's so it can check to see if it actually hit something--then play. explode! So that would be our "kaplew" sound. So now we've got the ability to kill an alien. So we're ready to check our win or lose conditions. So let's go below our Draw & Render Because we want that to happen first before we win or lose. So below this, let's add a win or lose section. Scroll this a little bit more so you can see. If invaders were all killed. Here's our win condition, because we always want to, you know, tie goes to the player. audio.play("win") Great. Now we need to break out of our game loop because we don't have menus or anything like that. We're just exiting and then our lose condition if invader's reached the bottom. Then we need to play lose. And this time you really, really lost because they got to the bottom. Killed you, presumably. So break out of the game loop, again. So now we have all of our logic. Let's go see if it runs. Did I do this correctly? cargo run [musically] "Bum budda bum!" Shoot. Oh, missed. [laser and explosion sounds] It works. [laser and explosion sounds] [more laser and explosion sounds] OK, so let's check our win and lose conditions. So I'm going to quit out. [lose sound] I'm going to come back to our code. And let's go over to invaders and let's come back up here to line 29 and add a little bit more logic. So let's only allow a few invaders, so--and x is exactly two. Let's go give that a try. [startup sound] Ah, much easier to win. Okay, let's give that a try. [laser and explosion sounds] [win sound] "You win." Hey, there we go! There's our win condition. Great. So now we can go back, undo that. Let's check our lose condition. Well, if they move really fast, that's going to be hard. So let's set this to our minimum time here, 250. cargo run. [startup sound] Holy cow! Okay. Come on! We can do this! [laser and explosion sounds] Let's hold this down. [laser and explosion sounds] Pew, pew. [laser and explosion sounds] Holding down stopped working. [laser and explosion sounds] Dunno what happened there. [laser and explosion sounds] Oh the situation is dire. [laser and explosion sounds] [losing sound] "You lose" Okay, our lose condition works as well. Now we've tested everything and that's it! That's the project! Go undo that last thing, and we've got our final state for the project. Now, if you'd like to take this further, please do! Experiment that with this. Add some more features. Keep track of a score. Change the playing field dimensions. If you want to get really fancy, you could redo some of the basic logic-- instead of having a single-character, ship and single-character aliens, you could try to make them multi-character. instead of having a single-character, ship and single-character aliens, you could try to make them multi-character. You will need to redo a lot of logic if you go that way! Anyway, if you do some of this and you'd like to contribute it back to the project, please go ahead and make a pull request. I'm fine altering this project further. You can always get back to these six stages in these videos by checking out the tags part-1 through part-6. You can always get back to these six stages in these videos by checking out the tags part-1 through part-6. Really easy. Just "git checkout" that tag name. Just "git checkout" that tag name. Thank you so much! I hope you enjoyed this project. :-) 35. Final Words: We've played with all the toys in the tool box. Now I want to know: What do you think about this course? What did you like? What didn't you like? What would you change? What would you add? I really enjoyed making this course and I hope you enjoyed taking it! May your journey with Rust be a Grand Adventure!